词向量入门---如何让计算机理解语言
概述
本文整理自BiliBli的《孔工码字》, 这是一个很好的视频号。讲的非常好,整理在这里,自己学习
他的Gitee地址:https://gitee.com/kongfanhe
从机器翻译到自动客服,从文生视频到AI小说,语言模型已经无所不在。
但计算机是如何理解语言的,
毕竟,计算机擅长的是数学计算,而人类语言是完全不同的信息载体
如何建立两者之间的联系,是一个非常重要的问题。
语言的基本单位是词
,这里的词可以是英文单词,也可以是是中文汉字。
但无论多复杂的句子,都可以拆解为词的排列组合。
The secret of getting ahead is getting started.
文字是人与人沟通的工具。
想让计算机理解语言,首先就要让它理解词的含义, 或者说把词映射到数学空间
人类对词的理解:
一个汉字信,有多种含义
词的含义是复杂和多样的, 需要从很多个维度来理解一个词。这种特性对应了数学中的向量
。
一个词就是一个多维向量
, 要建立词与向量之间的关系, 这个过程叫嵌入
, 这个向量被称为词向量
-
创建词汇表
嵌入
的第一步是建立一个词汇表
。包含了计算机要理解的所有词。
要创建词汇表,首先要有语料库
,可以是海量新闻,小说等文本,从中便利搜索出所有出现过的词,再按照词频排序,找到前五百个构成词汇表。
词汇表终究是有限的,如果遇到不认识的词,可以用特殊符号来代替,与人类思考方式类似, 遇到不认识的词,跳过它去理解上下文。
有了词汇表,如何给词找到向量?
简单的说,通过一个语言任务把词向量训练出来。
考虑下面的句子:
文字是人与人沟通的工具。
从中截取连续六个汉字,或六个词, 希望通过前五个预测第六个。通过文字是人与
这五个词来预测人
这个词。
有了词汇表,可以找到每个词的序号,任务变成了通过序号预测下一个序号。
在神经网络中,常用热编码表示序号,如序号56可以变成一个501维的向量。只有第56个元素是1,其他都为0,向量的维度,等于词汇表的长度。
对所有词进行类似的变换,,任务编程通过前5个预测第6个向量
理论上,热编码也是一种向量,但它远远不是我们想要的词向量,因为它没有语言含义,0和1仅仅表示词汇表中的序号而已。
-
搭建神经网络
现在开始,搭建神经网络,完成预测任务。
输入是五个热编码向量,输出是一个热编码向量,是一个典型的分类问题。
如果是一般的神经网络,会直接合并这五个输入向量,得到2505个节点,再送给线性全连接。
但为了获得词向量,需要对网络做一点修改:
首先,让每个向量单独通过一个全连接并输出十维向量
其次,保证这五个全连接是一样的, 相当于五个词依次通过同一个全连接,得到五个输出
合并输出,得到五十个节点,
再送给分类网络,得到被预测词的概率分布。
铜鼓训练,不断提高正确率,最终得到具备预测能力的神经网络。
** 为什么要这样设计网络呢,因为这保证了所有词在第一步经过同样的映射,这个映射正是嵌入
这里的10维输出, 正是词向量
为什么它是词向量
,可以这样理解: 如果两个词含义相近,则在输入中相互替代,不会影响预测结果。
从这个切面来看,这两个词输出就必须相似,所以这个向量是密切相关的,也就是词向量。
我们看到,虽然这个网络输出的是预测结果,但实际上,我们并不关心预测的准确率,因为真正需要的是网络中间层的词向量**。
-
词向量的余弦相似度
怎样评估词向量呢,介绍一个概念: 余弦相似度
在二维平面里,两个向量夹角的余弦等于它们的点积除以各自的模,它的取值范围是-1到1。
为1时,两个向量相同,也是最相似的。
对于两个词向量,我们也用余弦值来评估相似度。
比如:从直觉上,英文单词was与be动词is,are, were, been等接近,而汉字一应该与同为量词的二,三,四,五,头,首等接近。
这些词向量之间的余弦值应该接近于1
用PyTorch搭建一个神经网络
使用新闻联播和维基百科语料,训练出中文和英文词向量。
实现
代码
# 导入必要的模块
import os
import re
import torch
import sys
import numpy as np
# 定义上下文窗口大小,用于通过前5个词做预测
CONTEXT = 5
# 定义词向量的维度为10维
# 维度不能太大,否则词向量分布太稀疏
# 也不能太小,否则无法表达丰富语义
EMB_DIM = 10
# 定义一个函数,用于读取指定语言的文本数据
def read_text(language):
# 根据语言名称生成对应的文本数据目录
text_dir = f"data_{language}"
# 初始化一个空列表,用于存储读取到的文本行
lines = []
# 遍历指定目录下的所有文件
for f in os.listdir(text_dir):
# 过滤掉非txt文件
if not f.endswith(".txt"):
continue
# 构建文件的完整路径
f_path = os.path.join(text_dir, f)
# 打开文件并读取内容
with open(f_path, encoding="utf-8", errors="ignore") as file:
text = file.read()
# 如果是中文数据
if language == "cn":
# 利用正则表达式将文本按非汉字字符分割成句子
# 汉字的Unicode范围是 \u4e00 - \u9fff
for s in re.split(r"[^\u4e00-\u9fff]+", text):
# 将分割后的句子拆分成单个汉字并添加到lines列表中
lines.append(list(s))
# 如果是英文数据
elif language == "en":
# 利用正则表达式将文本按非小写字母和空格字符分割成句子
for s in re.split(r"[^a-z\s]+", text.lower()):
# 再将句子按空格分割成单词
ss = re.split(r"\s+", s)
# 过滤掉空字符串,并将剩余的单词添加到lines列表中
lines.append([x for x in ss if len(x) > 0])
return lines
# 定义一个函数,用于从文本行中提取单词数据
def read_words(lines):
# 初始化一个空列表,用于存储训练数据
data = []
# 初始化一个空列表,用于存储所有出现过的单词
all_words = []
# 遍历每一行文本
for line in lines:
# 检查当前行的长度是否小于上下文窗口大小加1
if len(line) < CONTEXT + 1:
# 如果小于,则跳过当前行
continue
# 遍历当前行,提取长度为上下文窗口大小加1的子序列
for i in range(0, len(line) - CONTEXT):
# 将子序列添加到训练数据列表中
data.append(line[i:i + CONTEXT + 1])
# 将当前行的所有单词添加到all_words列表中
all_words += line
return data, all_words
# 定义一个函数,用于生成词汇表
def generate_glossary(all_words):
# 首先把所有的词放在一起,转成set,剔除掉重复的词
# 初始化一个字典,用于记录每个单词的出现次数
count = {w: 0 for w in set(all_words)}
# 遍历所有单词
for w in all_words:
# 对词频计数
count[w] = count[w] + 1
# 按照词频从高到底取出前五百个作为词汇表
# 此处代码未完成,后续应该添加排序和取前500个单词的逻辑
words = np.array(list(count.keys()))
words = words[np.argsort(list(count.values()))[::-1]]
# '#'表示不认识的词
vocab = list(words[:500]) + ["#"]
return vocab
def get_text_data(language):
lines = read_text(language)
data, all_words = read_words(lines)
# 生成词汇表
vocab = generate_glossary(all_words)
return data, vocab
# 定义神经网络模型
class Embedder(torch.nn.Module):
# vocab: 词汇表
def __init__(self, vocab):
super().__init__()
# 把词汇表转成字典形式: key:词, value:序号
self.vocab_dict = {vocab[i]: i for i in range(len(vocab))}
# 创建网络的第一层: embedding层
# 本质上是一个线性全连接,不同之处在于,文本序列中每一个词都经过相同的映射
# pytorch将该过程封装到torch.nn.Embedding
# 参数1: 词汇表长度501
# 第二个参数[out]:词向量的维度, 实际上还隐藏了一个此序列长度5的维度
# 因为我们根据前5个词做预测
# 所以embedding层真正的输出是:词向量维度10,乘以序列长度5,等于50
self.embedding = torch.nn.Embedding(len(vocab), EMB_DIM)
# 下一层是个线性全连接, 连接embedding层的五十个节点
# 输出为第6个词的概率, 节点数对应词汇表的501种可能
self.linear = torch.nn.Linear(CONTEXT * EMB_DIM, len(vocab))
# 检测并配置显卡
if torch.cuda.is_available():
self.device = torch.device("cuda:0")
else:
self.device = torch.device("cpu")
self.to(self.device)
# 定义了神经网络的前向传播
# 输入是前5个词的id
def forward(self, context_ids):
embeds = self.embedding(context_ids)
# 对embedding层的输出结果进行调整
# 实际上是把5个词向量排列在一起
out = embeds.view(-1, CONTEXT * EMB_DIM)
out = self.linear(out)
# 输出为第6个词的对数概率分布
return torch.nn.functional.log_softmax(out, dim=1)
# 字符串列表转换成序号列表并替换陌生此
def words_to_ids(self, words):
ids = []
for w in words:
if w not in self.vocab_dict:
w = "#"
ids.append(self.vocab_dict[w])
return ids
# 对网络进行前向传播, 只传播embedding这一层
# 所以它的输出就是词向量
def embed(self, words):
ids = self.words_to_ids(words)
ids = torch.tensor(ids, dtype=torch.long).to(self.device)
# 由于这个函数自会被外部调用,不会用来做训练
# 所以需要把数据从pytorch中脱离, 在转为CPU上的numpy,便于存储
return self.embedding(ids).detach().cpu().numpy()
# 损失函数
# data: 一个batch的数据, 是一个列表的列表,
# eg:[['was', 'a', 'state', 'trunkline', 'highway', 'in'],..]
# 内层列表是训练用的6个词
# 外层列表batch的个数
def loss_acc(self, data):
# 首先把词序列转换为id序列
ids = [self.words_to_ids(words) for words in data]
# 把数据加载为张量
ids = torch.tensor(ids, dtype=torch.long).to(self.device)
# 数据的前5个词作为输入, 最后一个词为真实值,也就是label
context_ids, target_ids = ids[:, :CONTEXT], ids[:, -1]
# 通过forward函数,前向传播,获得对数概率
log_probs = self.forward(context_ids)
# 再是哟个NIILOSS计算损失的数值
loss = torch.nn.NLLLoss()(log_probs, target_ids)
# 除此之外,这个函数也计算当前的正确率
# 因为损失函数并不是很直观,而我们想指导正确率是多少,
# 首先,对输出概率取最大值, 得到预测结果
predicts = torch.argmax(log_probs, dim=1)
# 再与真实值对比,得到正确的个数
n_correct = torch.sum(predicts == target_ids).item()
# 最后得到准确率
accuracy = n_correct / len(data)
return loss, accuracy
def train(language):
# 定义训练30论,每个批次20000一组
epochs, batch = 30, 20000
# 取出数据
data, vocab = get_text_data(language)
# 模型初始化
model = Embedder(vocab)
# 定义优化器
optimizer = torch.optim.Adam(model.parameters(), lr=1e-2)
# 在每个轮次中,按照batch取出数据
for epoch in range(epochs):
print(f"{len(data) - batch}, {batch}")
for i in range(0, len(data) - batch, batch):
# 充值梯度
model.zero_grad()
# 计算损失
loss, acc = model.loss_acc(data[i:i + batch])
# 反向传播
loss.backward()
# 更新参数
optimizer.step()
# 在每个轮次的第0个batch上,打印损失数值和准确率
if i == 0:
print(i)
print(epoch, loss.item(), acc)
# 训练完成后,用embed函数把全部词汇表输入给神经网络
# 得到词汇表中,每个词对应的词向量
embed = model.embed(vocab)
# 保存词汇表和词向量
open(language + "_vocab.txt", "w").write("\n".join(vocab))
np.savetxt(language + "_embed.txt", embed, delimiter=",")
# 测试词向量的余弦值
def test(language):
# 从本地保存的文件中导入词汇表和词向量
vocab = open(language + "_vocab.txt").read().split("\n")
embed = np.loadtxt(language + "_embed.txt", delimiter=",")
# 从词汇表中取出词频最高的10个词
for w in vocab[:10]:
# 取出对应的词向量
vec = embed[vocab.index(w), :]
norm_emb = np.linalg.norm(embed, axis=1)
norm_vec = np.linalg.norm(vec)
cos = np.dot(embed, vec) / norm_emb / norm_vec
near = ""
# 找出余弦距离最近的五个词打印出来
for i in np.argsort(cos)[::-1][:5]:
near += vocab[i] + "_" + str(round(cos[i], 2)) + " "
print(w, ":", near, "\n")
if __name__ == "__main__":
if len(sys.argv) == 3 and sys.argv[1] == "train":
train(sys.argv[2])
elif len(sys.argv) == 3 and sys.argv[1] == "test":
test(sys.argv[2])
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?