词向量入门---如何让计算机理解语言

概述

本文整理自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])

资料

posted @   荣--  阅读(5)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示