首页
视频
资源
登录
原
HugginFace 中文命名实体识别(学习笔记)
1564
人阅读
2023/10/31 21:23
总访问:
2993268
评论:
0
收藏:
0
手机
分类:
HuggingFace
![](https://img.tnblog.net/arcimg/hb/782c293bc3904ab0bb30af5ff454beae.png) >#HugginFace 中文命名实体识别(学习笔记) [TOC] ## 任务简介 tn2>简单来说就是的识别人名、机构名、地名。 ## 数据集的介绍 tn2>本章所使用的数据集是`people_daily_ner`数据集。 `people_daily_ner`数据集标签对照表如下所示。 | 标签 | 描述 | | ------------ | ------------ | | `O` | 表示不属于一个命名实体 | | `B-PER` | 表示人名的开始。 | | `I-PER` | 表示的人名的中间和结尾部分。 | | `B-ORG` | 表示组织机构名的开始部分。 | | `I-ORG` | 表示组织机构名的中间和结尾部分。 | | `B-LOC` | 表示地名的开始。 | | `I-LOC` | 标识地名的中间和结尾部分。 | ## 实现代码 ### 准备数据集 #### 使用编码工具 tn2>`hfl/rbt3`编码器的编码结果与`bert-base-chinese`编码器相同,使用`hfl/rbt3`编码基本不需要任何的学习过程,此处首先加载该编码器,代码如下: ```python # 加载编码器 from transformers import AutoTokenizer tokenizer = AutoTokenizer.from_pretrained('hfl/rbt3') tokenizer ``` ![](https://img.tnblog.net/arcimg/hb/50351dd5c5634f9eb1868e8aae34efe3.png) tn2>进行试算一次,更加清晰的观察编码工具的输入和输出: ```python # 编码测试 out = tokenizer.batch_encode_plus( [[ '海', '钓', '比', '赛', '地', '点', '在', '厦', '门', '与', '金', '门', '之', '间', '的', '海', '域', '。' ], [ '这', '座', '依', '山', '傍', '水', '的', '博', '物', '馆', '由', '国', '内', '一', '流', '的', '设', '计', '师', '主', '持', '设', '计', '。' ]], truncation=True, padding=True, return_tensors='pt', max_length=20, is_split_into_words=True) #还原编码为句子 print(tokenizer.decode(out['input_ids'][0])) print(tokenizer.decode(out['input_ids'][1])) for k, v in out.items(): print(k, v) ``` ![](https://img.tnblog.net/arcimg/hb/9a319cd68c6646cc91b669e9c0ea9c17.png) | 参数 | 描述 | | ------------ | ------------ | | `is_split_into_words` | 当为`True`时表示告诉编码器输入的句子是分好的词的,不需要进行分词工作了。 | | `max_length` | 为`20`时,表示长度最长为20,不足补`[PAD]`,超出会被截断。 | #### 定义数据集 tn2>数据集为`people_daily_ner`。 ```python #第10章/定义数据集 import torch from datasets import load_dataset, load_from_disk class Dataset(torch.utils.data.Dataset): def __init__(self, split): #在线加载数据集 dataset = load_dataset(path='peoples_daily_ner', split=split) #离线加载数据集 #dataset = load_from_disk( # dataset_path='./data/peoples_daily_ner')[split] self.dataset = dataset #dataset.features['ner_tags'].feature.num_classes #7 #dataset.features['ner_tags'].feature.names #['O', 'B-PER', 'I-PER', 'B-ORG', 'I-ORG', 'B-LOC', 'I-LOC'] def __len__(self): return len(self.dataset) def __getitem__(self, i): tokens = self.dataset[i]['tokens'] labels = self.dataset[i]['ner_tags'] return tokens, labels dataset = Dataset('train') tokens, labels = dataset[0] print(tokens), print(labels) len(dataset) ``` tn2>这里我使用的是在线加载,在`people_daily_ner`数据集中,每条数据包括两个字段,即`tokens`和`ner_tag`,分别代码句子和标签,在`__getitem__()`函数中的两个字段取出来并返回即可。 运行结果如下: ![](https://img.tnblog.net/arcimg/hb/50f0efc1f9fc4b9aac97887c55537ee0.png) tn2>可见,训练数据集包括`20865`条数据,每条数据包括一条分好的词的文本和一个标签列表。 值得注意的是,此处的数据任然是文本数据,还没有被编码器编码。 #### 定义计算机设备 ```python device = 'cpu' if torch.cuda.is_available(): device = 'cuda' device ``` ![](https://img.tnblog.net/arcimg/hb/76ce5701edf9450fb85c78a7b89bce8d.png) #### 定义数据整理函数 tn2>做一个数据整理的操作。 ```python #第10章/定义数据整理函数 def collate_fn(data): tokens = [i[0] for i in data] labels = [i[1] for i in data] #编码 inputs = tokenizer.batch_encode_plus(tokens, truncation=True, padding=True, return_tensors='pt', max_length=512, is_split_into_words=True) #求一批数据中最长的句子长度 lens = inputs['input_ids'].shape[1] #在labels的头尾补充7,把所有的labels补充成统一的长度 for i in range(len(labels)): labels[i] = [7] + labels[i] labels[i] += [7] * lens labels[i] = labels[i][:lens] #把编码结果移动到计算设备 for k, v in inputs.items(): inputs[k] = v.to(device) #把统一长度的labels组装成矩阵,并移动到计算设备 labels = torch.LongTensor(labels).to(device) return inputs, labels ``` tn2>使用`tokenizer`的工具对 tokens 进行编码。`tokenizer.batch_encode_plus` 函数将 `tokens` 编码成模型可以处理的格式,同时确保它们具有相同的长度。这个函数执行以下操作: `truncation=True`:截断文本以确保所有文本的长度不超过指定的最大长度。 `padding=True`:在文本的末尾添加填充标记,以确保它们的长度相等。 `return_tensors='pt'`:返回 PyTorch 张量。 `max_length=512`:限制文本长度不超过512个标记。 `is_split_into_words=True`:表示 tokens 已经分割成单词。 接下来进行试算一下: ```python #数据整理函数试算 #模拟一批数据 data = [ ([ '海', '钓', '比', '赛', '地', '点', '在', '厦', '门', '与', '金', '门', '之', '间', '的', '海', '域', '。' ], [0, 0, 0, 0, 0, 0, 0, 5, 6, 0, 5, 6, 0, 0, 0, 0, 0, 0]), ([ '这', '座', '依', '山', '傍', '水', '的', '博', '物', '馆', '由', '国', '内', '一', '流', '的', '设', '计', '师', '主', '持', '设', '计', ',', '整', '个', '建', '筑', '群', '精', '美', '而', '恢', '宏', '。' ], [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]), ] #试算 inputs, labels = collate_fn(data) for k, v in inputs.items(): print(k, v.shape) print('labels', labels.shape) ``` ![](https://img.tnblog.net/arcimg/hb/5a09c217887d4c448c79d408eb5cad78.png) tn2>当前最长的句子有37个词。 #### 定义数据集加载器 ```python #数据加载器 loader = torch.utils.data.DataLoader(dataset=dataset, batch_size=16, collate_fn=collate_fn, shuffle=True, drop_last=True) len(loader) ``` ![](https://img.tnblog.net/arcimg/hb/378b3a5ff43f4a8399ee16bde47efb7c.png) tn2>可见训练数据集一共执行了1340个批次。 定义好了数据集加载器之后,可以查看一批数据样本,代码如下: ```python #第10章/查看数据样例 for i, (inputs, labels) in enumerate(loader): break print(tokenizer.decode(inputs['input_ids'][0])) print(labels[0]) for k, v in inputs.items(): print(k, v.shape) ``` ![](https://img.tnblog.net/arcimg/hb/b940b32e5f3e4c9aa0de766ffaddc557.png) ### 定义模型 #### 加载预训练模型 tn2>使用`hfl/rbt3`作为预习训练模型,代码如下: ```python #加载预训练模型 from transformers import AutoModel pretrained = AutoModel.from_pretrained('hfl/rbt3') pretrained.to(device) #统计参数量 print(sum(i.numel() for i in pretrained.parameters()) / 10000) ``` ![](https://img.tnblog.net/arcimg/hb/89ec72a5cb0e4d6981a90718ee7344ff.png) tn2>定义好预训练模型之后,可以进行一次试算: ```python #第10章/模型试算 #[b, lens] -> [b, lens, 768] pretrained(**inputs).last_hidden_state.shape ``` ![](https://img.tnblog.net/arcimg/hb/2973d0b01b594801ae998933fcb34849.png) tn2>样式数据为16句话的编码结果,从预训练模型的计算结果可以看出,这也是16句话的结果,每句话包括54个词,每个词被抽成了一个768维的向量。到此为止,通过预训练模型成功的把16句话转换成一个特征向量矩阵,可以接入下游任务模型做分类或回归任务。 #### 定义下游任务模型 tn2>本章将使用两段式训练,所以要求下游任务模型能够切换微调(Fine Tuning)模式。 tn>什么是两段式训练?两段式训练是一种训练计较,指先单独对下游任务模型进行一定的训练,待下游任务模型掌握了一定的知识以后,再连通预训练模型和下游模型一起进行训练的模式。 ```python #第10章/定义下游模型 class Model(torch.nn.Module): def __init__(self): super().__init__() #标识当前模型是否处于tuning模式 self.tuning = False #当处于tuning模式时backbone应该属于当前模型的一部分,否则该变量为空 self.pretrained = None #当前模型的神经网络层 self.rnn = torch.nn.GRU(input_size=768, hidden_size=768, batch_first=True) self.fc = torch.nn.Linear(in_features=768, out_features=8) def forward(self, inputs): #根据当前模型是否处于tuning模式而使用外部backbone或内部backbone计算 if self.tuning: out = self.pretrained(**inputs).last_hidden_state else: with torch.no_grad(): out = pretrained(**inputs).last_hidden_state #backbone抽取的特征输入rnn网络进一步抽取特征 out, _ = self.rnn(out) #rnn网络抽取的特征最后输入fc神经网络分类 out = self.fc(out).softmax(dim=2) return out #切换下游任务模型的的tuning模式 def fine_tuning(self, tuning): self.tuning = tuning #tuning模式时,训练backbone的参数 if tuning: for i in pretrained.parameters(): i.requires_grad = True pretrained.train() self.pretrained = pretrained #非tuning模式时,不训练backbone的参数 else: for i in pretrained.parameters(): i.requires_grad_(False) pretrained.eval() self.pretrained = None model = Model() model.to(device) model(inputs).shape ``` tn2>这段代码定义并初始化了下游任务模型,在下游任务模型的`__init_()`函数中有两个重要的变量,即`tuning`和`pretrained`,其中`tuning`为布尔型变量,取值为`True`和`False`,它表明了当前模型是否处于微调模型,默认值为`False`,即非微调模式。`pretrained`代表了预训练模型,当前处于微调模型时预训练模型应该属于当前模型的一部分,反之则不属于,默认为`None`,即预训练模型不属于当前模型的一部分。<br/> 在`__init__()`函数中还定义了下游任务模型的两个网络层,即是循环神经网络层和全连接神经网络层,分别命名为`rnn`和`fc`,其中循环神经网络的实现为GRU网络。<br/> `forward()`函数定义了下游任务模型的计算过程,首先判断当前模型是否处于微调模式,如果处于微调模式的话使用内部的预训练模型,否则使用外部的预训练模型,并且不计算预训练模型的梯度。得到预训练模型抽取的文本特征后,把文本特征输入循环神经网络进一步抽取特征,最后把特征数据输入全连接神经网络分类即可。<br/> 为什么需要循环神经网络层?这是一个想当然的想法,因为标签列表也可以看作一句话,这句`话`也符合一定的统计规律,例如人名的中间部分(I-PER)一定出现在人名的开头(B-PER之后),所以把预训练模型抽取的文本特征也当作一个序列数据进行处理,输入循环神经网络再次抽取特征,最后做分类计算,期望可以得到更好的结果。读者也可以尝试移除,或者增加其他的层,来提高模型预测的正确率,深度学习任务中往往有很多这样的尝试性实验。 一般的PyTorch模型定义`__init__()`函数和`forward()`函数就可以了,但是在上面的模型中还定义了`fine_tuning()`的函数,这个函数就是要切换下游模型的微调模型,入参为一个布尔值,取值为`True`和`False`。如前所述,当切换到微调模式时,把预训练模型切换到训练模型。 反之,不处于微调模式时要冻结预训练模型的参数,不让它们随着训练更新,并且预训练模型不属于下游任务模型的一部分,要把训练模式切换为运行模式。 下游任务模型试算结果如下: ![](https://img.tnblog.net/arcimg/hb/ffccf96ac43140d1bd83fdf83a29331f.png) tn2>从结果可以看出,运算的结果为16句话,54个词,每个词为8分类结果。 ### 训练和测试 #### 两个工具函数 tn2>为了便于后续的训练和测试,需要定义两个工具函数,第一个函数的功能是对计算结果和labels变形,并且移除`PAD`,需要这个函数的原因是因为在一批数据中,往往会有很多的`PAD`,对这些`PAD`去计算它们的实体命名是没有什么意义的,显然它们不可能是任何命名实体,为了不让模型去研究这些`PAD`是什么东西,直接从计算结果中移除这些`PAD`,以防止模型做无用功。实现代码如下: ```python #第10章/对计算结果和label变形,并且移除pad def reshape_and_remove_pad(outs, labels, attention_mask): #变形,便于计算loss #[b, lens, 8] -> [b*lens, 8] outs = outs.reshape(-1, 8) #[b, lens] -> [b*lens] labels = labels.reshape(-1) #忽略对pad的计算结果 #[b, lens] -> [b*lens - pad] select = attention_mask.reshape(-1) == 1 outs = outs[select] labels = labels[select] return outs, labels reshape_and_remove_pad(torch.randn(2, 3, 8), torch.ones(2, 3), torch.ones(2, 3)) ``` tn2>这段的代码中,首先把模型的预测结果和labels都从多句话合并成一句话,合并的方式就是简单地进行头尾相接,这样能够方便后续计算loss。 移除`PAD`时使用编码结果中的`attention_mask`,`attention_mask`标记了一个句子中哪些的位置是`PAD`,`attention_mask`中只有0和1,其中0标识是其中`PAD`的位置,使用`attention_mask`可以很轻松地过滤掉结果中的`PAD`. 在代码的最后使用一批虚拟的数据试算该函数,运行结果如下: ![](https://img.tnblog.net/arcimg/hb/4d16caae2d534107bd7fcf1a917da7c4.png) tn2>虚拟数据中的`2x3x8`矩阵表示2句话、3个词、每个词8分类的预测结果,第1个`2x3`矩阵表示真实的`labels`,第二个`2x3`的矩阵表示`attention_mask`,因为全为1,所以全部保留,没有`PAD`。 最后计算的结果也确实全部保留了预测结果和`labels`,并且预测结果和`labels`被变形成一句话,和预测一致。 第二个函数用于预测结果中测试正确了多少个,以及一共有多少个预测结果如下: ```python #第10章/获取正确数量和总数 def get_correct_and_total_count(labels, outs): #[b*lens, 8] -> [b*lens] outs = outs.argmax(dim=1) correct = (outs == labels).sum().item() total = len(labels) #计算除了0以外元素的正确率,因为0太多了,包括的话,正确率很容易虚高 select = labels != 0 outs = outs[select] labels = labels[select] correct_content = (outs == labels).sum().item() total_content = len(labels) return correct, total, correct_content, total_content get_correct_and_total_count(torch.ones(16), torch.randn(16, 8)) ``` ![](https://img.tnblog.net/arcimg/hb/542c0d215d964a73883ac3eca6765e57.png) tn2>因为虚拟的labels全是1,并没有出现标签0的情况,所以统计得出的两套正确数量和总数相等。 #### 训练 ```python #第10章/训练 from transformers import AdamW from transformers.optimization import get_scheduler def train(epochs): lr = 2e-5 if model.tuning else 5e-4 optimizer = AdamW(model.parameters(), lr=lr) criterion = torch.nn.CrossEntropyLoss() scheduler = get_scheduler(name='linear', num_warmup_steps=0, num_training_steps=len(loader) * epochs, optimizer=optimizer) model.train() for epoch in range(epochs): for step, (inputs, labels) in enumerate(loader): #模型计算 #[b, lens] -> [b, lens, 8] outs = model(inputs) #对outs和label变形,并且移除pad #outs -> [b, lens, 8] -> [c, 8] #labels -> [b, lens] -> [c] outs, labels = reshape_and_remove_pad(outs, labels, inputs['attention_mask']) #梯度下降 loss = criterion(outs, labels) loss.backward() optimizer.step() scheduler.step() optimizer.zero_grad() if step % (len(loader) * epochs // 30) == 0: counts = get_correct_and_total_count(labels, outs) accuracy = counts[0] / counts[1] accuracy_content = counts[2] / counts[3] lr = optimizer.state_dict()['param_groups'][0]['lr'] print(epoch, step, loss.item(), lr, accuracy, accuracy_content) torch.save(model, 'model/中文命名实体识别.model') ``` tn2>训练函数接受一个参数`epochs`,表示要使用全量数据训练几个轮次,由于是两段式训练,所以得出来的训练轮次可能不一样。所以需要这个参数。 由于采用了两段式训练,所以会根据模型是否处于微调模式选择不同的Learning Rate,在非微调模式时选择较大的Learning Rate,以快速训练下游任务模型在微调模式时则选择较小的Learning Rate,以精细地调节模型参数,帮助模型优化到更优的性能。 之后定义了优化器、loss计算函数、学习率调节器。 需要注意的是,优化器优化的参数表为下游任务模型的所有参数,因为下游任务模型在微调模式的问题,在非微调模式下,预训练模型并不属于下游任务模型的一部分,所以优化器的参数数量比较少,仅包含下游任务模型本身的参数。而在微调模式下,预训练模型属于下游任务模型的一部分,所以优化器优化的参数表也会包括预训练模型,这也是为什么要在切换微调模式时,设置下游任务模型的pretrained属性的原因。 接下来把下游任务模型切换到训练模式,并且在全量训练数据上遍历epochs个轮次,对模型进行训练。训练过程如下所述: (1)从训练集加载器中获取一个批次的数据。 (2)让模型计算预测结果 (3)使用工具函数对预测结果和labels进行变形,移除预测结果和labels中的PAD。 (4)计算loss并执行梯度下降优化模型。 (5)每隔一定的steps,输出一次模型当前的各项数据,便于观察 (6)每训练完一个epoch,将模型的参数大家保存到磁盘。 #### 两段式训练 ```python #两段式训练第1步,训练下游任务模型 model.fine_tuning(False) print(sum(p.numel() for p in model.parameters()) / 10000) train(1) ``` tn2>在这段代码中,首先把下游任务模型切换到非微调模式,之后输出了模型的参数量,由于干预训练模型并不属于下游任务模型的一部分,所以此处期待的参数量应该稍小,最后在全量数据上训练1个轮次,运行结果如下: ![](https://img.tnblog.net/arcimg/hb/bba3c95499a44e1a9fd06045cf4120d5.png) tn2>可以看到在非微调模式下,下游任务模型的参数量为354万。 训练过程的输出表如下: ![](https://img.tnblog.net/arcimg/hb/40b98091a2c046079905b633cd593595.png) tn2>从该表可以看出,随着训练步骤的增多,loss收敛得很快,并且正确率已经很高,即达到了`85%`,但排除`labels`中的0之后,正确率却只有`25%`,可见正确率是虚高的。 接下来可以进行两段式训练的第二个阶段,代码如下: ```python #第10章/两段式训练第2步,同时训练下游任务模型和预训练模型 model.fine_tuning(True) print(sum(p.numel() for p in model.parameters()) / 10000) train(5) ``` tn2>这段代码,把下游任务模式切换到微调模式,这意味着预训练模型将被一起训练。代码中输出了当前下游模型的参数量,由于预训练模型已经属于下游任务模型的一部分,因此此处的参数量期望会比较大,最后在全量数据上执行5个轮次的训练,运行结果如下: ![](https://img.tnblog.net/arcimg/hb/0a5b894f0fec4f42a25ccbc832d86596.png) tn2>可见切换到微调模式后,下游任务模型的的参数量增加到`4200`万个,由于采用了较小的的预训练模式,所以这个参数量的规模依然较小,即使在一颗CPU上训练这个任务,时间也应该在可接受的范围内。训练过程的输出见表: ![](https://img.tnblog.net/arcimg/hb/7184375bafdd48cd97910fa07bf21238.png) tn2>可以看出训练总体的正确率上升了不少,排除标签0之后的正确率也上升了。 #### 测试 tn2>最后,对训练好的模型进行训练,以验证训练的有效性,代码如下: ```python #第10章/测试 def test(): #加载训练完的模型 model_load = torch.load('中文命名实体识别.model') model_load.tuning = True model_load.eval() model_load.to(device) #测试数据集加载器 loader_test = torch.utils.data.DataLoader(dataset=Dataset('validation'), batch_size=128, collate_fn=collate_fn, shuffle=True, drop_last=True) correct = 0 total = 0 correct_content = 0 total_content = 0 #遍历测试数据集 for step, (inputs, labels) in enumerate(loader_test): #测试5个批次即可,不全部全部遍历 if step == 5: break print(step) #计算 with torch.no_grad(): #[b, lens] -> [b, lens, 8] -> [b, lens] outs = model_load(inputs) #对outs和label变形,并且移除pad #outs -> [b, lens, 8] -> [c, 8] #labels -> [b, lens] -> [c] outs, labels = reshape_and_remove_pad(outs, labels, inputs['attention_mask']) #统计正确数量 counts = get_correct_and_total_count(labels, outs) correct += counts[0] total += counts[1] correct_content += counts[2] total_content += counts[3] print(correct / total, correct_content / total_content) test() ``` tn2>在这段的代码中,首先从磁盘加载了训练完毕的模型,然后把模型切换到运行模式,再把模型移动到定义好的计算设备上。 完成模型的加载之后,定义测试数据集和加载器,并取出5个批次的数据让模型进行预测,最后统计两个正确率并输出,两个正确率之间的区别是一个统计了标签0,另一个则没有,运行结果如下: ![](https://img.tnblog.net/arcimg/hb/f0fcaf8b9d8345c3ad4d24df8b58bb77.png) #### 预测 tn2>验证了模型的有效性之后,可以进行一些预测,以更直观地观察模型的预测结果,代码如下: ```python #预测 def predict(): #加载模型 model_load = torch.load('中文命名实体识别.model') model_load.tuning = True model_load.eval() model_load.to(device) #测试数据集加载器 loader_test = torch.utils.data.DataLoader(dataset=Dataset('validation'), batch_size=32, collate_fn=collate_fn, shuffle=True, drop_last=True) #取一个批次的数据 for i, (inputs, labels) in enumerate(loader_test): break #计算 with torch.no_grad(): #[b, lens] -> [b, lens, 8] -> [b, lens] outs = model_load(inputs).argmax(dim=2) for i in range(32): #移除pad select = inputs['attention_mask'][i] == 1 input_id = inputs['input_ids'][i, select] out = outs[i, select] label = labels[i, select] #输出原句子 print(tokenizer.decode(input_id).replace(' ', '')) #输出tag for tag in [label, out]: s = '' for j in range(len(tag)): if tag[j] == 0: s += '·' continue s += tokenizer.decode(input_id[j]) s += str(tag[j].item()) print(s) print('==========================') predict() ``` tn2>这段代码中执行了以下工作: (1)加载了训练完毕的模型,并切换到运行模式,再移动到定义好的计算设备上。 (2)定义了测试数据集加载器,然后从数据集加载器中取出了一批数据。 (3)对这批数据进行预测。 (4)对原句子进行一些处理,以便符合人类对阅读习惯。 (5)输出labels和预测结果,以观察两种的异同。 ![](https://img.tnblog.net/arcimg/hb/5de52e6a94da4fc48a809f8acc43f1ae.png)
欢迎加群讨论技术,1群:677373950(满了,可以加,但通过不了),2群:656732739
👈{{preArticle.title}}
👉{{nextArticle.title}}
评价
{{titleitem}}
{{titleitem}}
{{item.content}}
{{titleitem}}
{{titleitem}}
{{item.content}}
尘叶心繁
这一世以无限游戏为使命!
博主信息
排名
6
文章
6
粉丝
16
评论
8
文章类别
.net后台框架
178篇
linux
18篇
linux中cve
1篇
windows中cve
0篇
资源分享
10篇
Win32
3篇
前端
28篇
传说中的c
5篇
Xamarin
9篇
docker
15篇
容器编排
101篇
grpc
4篇
Go
15篇
yaml模板
1篇
理论
2篇
更多
Sqlserver
4篇
云产品
39篇
git
3篇
Unity
1篇
考证
2篇
RabbitMq
23篇
Harbor
1篇
Ansible
8篇
Jenkins
17篇
Vue
1篇
Ids4
18篇
istio
1篇
架构
2篇
网络
7篇
windbg
4篇
AI
19篇
threejs
2篇
人物
1篇
嵌入式
20篇
python
20篇
HuggingFace
8篇
pytorch
10篇
opencv
6篇
Halcon
5篇
最新文章
最新评价
{{item.articleTitle}}
{{item.blogName}}
:
{{item.content}}
关于我们
ICP备案 :
渝ICP备18016597号-1
网站信息:
2018-2024
TNBLOG.NET
技术交流:
群号656732739
联系我们:
contact@tnblog.net
欢迎加群
欢迎加群交流技术