基于 N-gram语法的文本生成教授的教学链接
https://www.kaggle.com/code/wongprofessor/n-gram-text-generation
add Codeadd Markdown
N-gram是自然语言处理中常用的技术,它可以用于文本生成、语言模型训练等任务。本文将介绍什么是n-gram,如何在Python中实现n-gram文本生成,并提供丰富的示例代码来帮助大家更好地理解和应用这一技术。 什么是N-gram?
N-gram是自然语言处理中的一种文本建模技术,用于对文本数据进行分析和生成。它是一种基于n个连续词语或字符的序列模型,其中n表示n-gram的大小。通常,n的取值为1、2、3等。
Unigram(1-gram):一个单词或一个字符为一个单位。例如,"I", "love", "Python"。 Bigram(2-gram):两个相邻的单词或字符为一个单位。例如,"I love", "love Python"。 Trigram(3-gram):三个相邻的单词或字符为一个单位。例如,"I love Python"。N-gram模型通过分析文本中不同n-gram的出现频率,可以用于文本分类、文本生成、语言模型等任务。 实现N-gram文本生成
下面将演示如何在Python中实现N-gram文本生成。将使用一个简单的示例来说明这一过程。
add Codeadd Markdown
1. 准备文本数据
首先,需要准备一些文本数据,这将作为训练数据。这里使用了莎士比亚的一些文本作为示例数据,可以使用自己的文本数据。
text = """ To be or not to be, that is the question; Whether 'tis nobler in the mind to suffer The slings and arrows of outrageous fortune, Or to take arms against a sea of troubles And by opposing end them. To die---to sleep, No more; and by a sleep to say we end The heart-ache and the thousand natural shocks That flesh is heir to, 'tis a consummation Devoutly to be wish'd. To die, to sleep; To sleep, perchance to dream---ay, there's the rub, For in that sleep of death what dreams may come, When we have shuffled off this mortal coil, Must give us pause---there's the respect That makes calamity of so long life; The oppressor's wrong, the proud man's contumely, The pangs of despis'd love, the law's delay, The insolence of office, and the spurns That patient merit of the unworthy takes, When he himself might his quietus make With a bare bodkin? Who would these fardels bear, To grunt and sweat under a weary life, But that the dread of something after death--- The undiscover'd country, from whose bourn No traveller returns---puzzles the will, And makes us rather bear those ills we have Than fly to others that we know not of? Thus conscience does make cowards of us all; And thus the native hue of resolution Is sicklied o'er with the pale cast of thought, And enterprises of great pith and moment With this regard their currents turn awry, And lose the name of action. """ # 去掉换行符,并将文本转换为小写 text = text.replace('\n', ' ').lower()2. 创建N-gram模型
接下来,将创建一个N-gram模型,该模型可以接受一个文本字符串,并将其分割成n-gram序列。
def create_ngram_model(text, n): words = text.split() # 将文本分割成单词 ngrams = [] # 用于存储n-grams的列表 for i in range(len(words) - n + 1): ngram = ' '.join(words[i:i + n]) # 创建一个n-gram ngrams.append(ngram) return ngrams n = 2 # 选择2-gram模型 ngram_model = create_ngram_model(text, n) # 打印前10个2-grams print(ngram_model[:10])在上述示例中,定义了一个create_ngram_model函数,该函数接受文本和n值作为参数,并返回n-gram的列表。选择了2-gram模型(bigram),并打印了前10个2-grams。
['to be', 'be or', 'or not', 'not to', 'to be,', 'be, that', 'that is', 'is the', 'the question;', 'question; whether']3. 生成文本
有了N-gram模型后,可以使用它来生成新的文本。生成文本的方法是随机选择一个n-gram作为起始点,然后根据模型中的n-gram频率来选择接下来的n-gram,依此类推,直到生成所需长度的文本。
import random def generate_text(ngram_model, n, length=50): generated_text = random.choice(ngram_model) # 随机选择一个n-gram作为起始点 words = generated_text.split() while len(words) < length: possible_next_ngrams = [ngram for ngram in ngram_model if ' '.join(words[-n + 1:]) in ngram] if not possible_next_ngrams: break next_ngram = random.choice(possible_next_ngrams) words.extend(next_ngram.split()) generated_text = ' '.join(words) return generated_text generated_text = generate_text(ngram_model, n, length=100) print(generated_text)在上述示例中,定义了一个generate_text函数,该函数接受N-gram模型、n值和所需生成文本的长度作为参数。它从模型中随机选择一个n-gram作为起始点,并根据模型中的n-gram频率选择接下来的n-gram,直到生成指定长度的文本。
does make quietus make quietus make makes us makes us thus the the rub, the rub, the rub, rub, for for in against a outrageous fortune, outrageous fortune, outrageous fortune, outrageous fortune, fortune, or be or more; and and lose and lose and lose lose the that the us rather us rather us rather rather bear rather bear rather bear bear those bear those those ills ills we we know we know we know know not not to not to to be to be, to be, to be, be, that others that that patient patient merit merit of of outrageous4.改进N-gram模型
虽然前面的示例中的N-gram模型能够生成文本,但它还有一些局限性。例如,它只考虑了相邻的n-gram,而没有考虑到更远的依赖关系。为了改进模型,可以考虑以下几种方法:
1)增加n-gram的大小
通过增加n-gram的大小(如3-gram或4-gram),模型可以捕捉更长范围的依赖关系,生成更具连贯性的文本。但需要注意,增加n-gram的大小也会增加模型的复杂度和数据需求。
# 增加n-gram的大小为3 n = 3 ngram_model = create_ngram_model(text, n)2)使用更多的训练数据(注:尝试用中文小说《家》作为语料) 模型的性能通常取决于训练数据的质量和数量。如果有更多的文本数据可用,可以使用更多的训练数据来训练模型,以提高其性能。
3)使用更高级的文本生成技术 N-gram模型是一种基本的文本生成技术,但在实际应用中可能需要更高级的方法,如循环神经网络(RNN)或变换器(Transformer)等。这些模型可以学习更复杂的语言结构,生成更具语法和语义的文本。
4)改进文本生成算法 改进文本生成算法可以使生成的文本更具连贯性和多样性。一种常见的方法是使用温度(temperature)参数来调整生成的文本多样性,较高的温度会生成更多的随机性,而较低的温度会生成更加确定性的文本。
注: 温度参数用于调节模型输出的随机性。取值范围通常在 0 到 1 之间,其本质是对模型生成下一个词的概率分布进行调整。当温度接近 0 时,模型倾向于选择概率最高的词,输出结果较为保守、确定,更符合常规逻辑与预期,适用于对准确性要求极高的场景,如知识问答、精确指令执行。例如在回答科学常识类问题时,低温度设置可确保答案精准无误。相反,当温度接近 1 时,概率分布更加均匀,模型会选择一些概率较低但具有创新性的词,输出更具多样性与创造性,可用于创意写作、头脑风暴等场景,像创作小说情节、构思广告文案时,较高温度能激发独特的想法 。
def generate_text_with_temperature(ngram_model, n, length=50, temperature=1.0): generated_text = random.choice(ngram_model) words = generated_text.split() while len(words) < length: possible_next_ngrams = [ngram for ngram in ngram_model if ' '.join(words[-n + 1:]) in ngram] if not possible_next_ngrams: break # 根据温度参数调整选择下一个n-gram的随机性 next_ngram = random.choices(possible_next_ngrams, weights=[1.0 / temperature] * len(possible_next_ngrams))[0] words.extend(next_ngram.split()) generated_text = ' '.join(words) return generated_text # 使用温度参数为0.5生成文本 generated_text = generate_text_with_temperature(ngram_model, n, length=100, temperature=0.5) print(generated_text) and the spurns and the spurns the spurns that the spurns that spurns that patient that patient merit that patient merit that patient merit that patient merit patient merit of merit of the of the unworthy the unworthy takes, unworthy takes, when unworthy takes, when takes, when he when he himself when he himself when he himself when he himself when he himself he himself might himself might his might his quietus might his quietus might his quietus might his quietus might his quietus might his quietus might his quietus might his quietus his quietus make quietus make with quietus make with
作业展示:
1.读文件和分词
with open('/kaggle/input/family/.txt', 'r', encoding='gbk') as f: text = f.read() # 这里把数据集内容读到了 text 变量由于中英文的差距,英语用空格分词,中文都是连续的,所以需要用滑块的办法来分词,根据教授给定的数据集,处理如下:
def create_chinese_bigram(text, n=2): # 直接按字符处理,不需要分词 bigrams = [] for i in range(len(text) - n + 1): bigram = text[i:i+n] # 过滤掉空格、换行等 if not any(c.isspace() for c in bigram): bigrams.append(bigram) return bigrams # 使用示例 bigrams = create_chinese_bigram(text, 2) print("前20个中文2-grams(相邻两字组合):") for i, bg in enumerate(bigrams[:100]): print(f"{i+1}: {bg}")得到:
前20个中文2-grams(相邻两字组合): 1: -- 2: -- 3: -- 4: -- 5: -- 6: -- 7: -- 8: -- 9: -- 10: -- 11: -- 12: -- 13: -- 14: -- 15: -- 16: -- 17: -- 18: -- 19: -- 20: -- 21: -- 22: -- 23: -- 24: -- 25: -- 26: -- 27: -- 28: -- 29: -- 30: -- 31: -- 32: -- 33: -- 34: -- 35: -- 36: -- 37: -- 38: -- 39: -- 40: -- 41: -- 42: -- 43: -- 44: -- 45: -- 46: -- 47: -- 48: -- 49: -- 50: -- 51: -- 52: -- 53: -- 54: -- 55: -- 56: -- 57: -- 58: -- 59: -- 60: -- 61: -- 62: -- 63: -- 64: -- 65: -- 66: -- 67: -- 68: -- 69: -- 70: -- 71: -- 72: -- 73: -- 74: -- 75: -- 76: -- 77: -- 78: -- 79: -- 80: 风刮 81: 刮得 82: 得很 83: 很紧 84: 紧, 85: ,雪 86: 雪片 87: 片像 88: 像扯 89: 扯破 90: 破了 91: 了的 92: 的棉 93: 棉絮 94: 絮一 95: 一样 96: 样在 97: 在空 98: 空中 99: 中飞 100: 飞舞2.生成文本
!pip install jieba import jieba import random # 1. 中文分词 def create_chinese_ngram_model(text, n=2): """创建中文词语级 n-gram 模型""" # 分词 words = list(jieba.cut(text)) # 生成 n-grams ngrams = [] for i in range(len(words) - n + 1): ngram = ' '.join(words[i:i + n]) # 用空格连接词语 ngrams.append(ngram) return ngrams, words # 2. 重新生成 n-gram chinese_ngrams, word_list = create_chinese_ngram_model(text, n=2) print(f"前10个词语级2-grams: {chinese_ngrams[:10]}") # 3. 中文文本生成函数 def generate_chinese_text(ngram_model, word_list, n=2, length=30): """基于词语级 n-gram 生成中文文本""" if not ngram_model: return "" # 随机选择一个起始 n-gram start = random.choice(ngram_model) generated_words = start.split() while len(generated_words) < length: # 获取最后 n-1 个词作为上下文 context = ' '.join(generated_words[-(n-1):]) # 查找可能的下一词 possible_next = [] for ngram in ngram_model: if ngram.startswith(context + ' '): next_word = ngram.split()[-1] possible_next.append(next_word) if not possible_next: break # 随机选择一个下一个词 next_word = random.choice(possible_next) generated_words.append(next_word) # 将词语列表连接成文本 return ''.join(generated_words) # 4. 生成文本 generated = generate_chinese_text(chinese_ngrams, word_list, n=2, length=100) print("\n生成的文本:") print(generated)原理是这样的:
原始文本: "风刮得很紧,雪片飞舞" 分词结果: ["风", "刮", "得", "很", "紧", ",", "雪片", "飞舞"] 2-gram列表:["风 刮", "刮 得", "得 很", "很 紧", "紧 ,", ", 雪片", "雪片 飞舞"] 生成过程: 1. 随机起点:"风 刮" → 已生成 ["风", "刮"] 2. 最后1词:"刮" → 匹配到 "刮 得" → 下一个词 "得" 3. 已生成 ["风", "刮", "得"] 4. 最后1词:"得" → 匹配到 "得 很" → 下一个词 "很" 5. 继续...然后得到:
Requirement already satisfied: jieba in /usr/local/lib/python3.11/dist-packages (0.42.1) 前10个词语级2-grams: ['家 \n', '\n \n', '\n ', ' 1', '1 \n', '\n \n', '\n --------------------------------------------------------------------------------', '-------------------------------------------------------------------------------- \n', '\n \n', '\n \u3000'] 生成的文本: 落下去了?他们不久,我们家里过的同学正因为他可以知道的伴侣一样,一阵声音里,就让我作一样)>>>>>>>>{{{和恨它比他按月,她才想起了两杯,却有把戏。他招来社会里面,回过头看,就是藏着碗碟家具略略抬起头,而且元宵节的意思向督座转达。他不仅大哥的面影。过了的咽喉被人。。。。。那观察可见,没有去除格式标记,都是无效的字符组合。
所以要清洗数据先:
import re # 先清洗文本 def clean_chinese_text(text): """ 清洗中文文本: 1. 去除数字编号(如 1、2、3) 2. 去除分隔线(如 -------) 3. 合并多余空白 4. 只保留中文、标点和必要空格 """ # 去除数字编号和分隔线 text = re.sub(r'\d+[::]?\s*', '', text) # 去除 "1:"、"2: " 等 text = re.sub(r'-{4,}', '', text) # 去除 -------- 分隔线 text = re.sub(r'\n{3,}', '\n\n', text) # 多个换行变两个 text = text.strip() # 去除首尾空白 return text # 使用清洗后的文本 clean_text = clean_chinese_text(text) print(f"清洗后文本长度: {len(clean_text)} 字符") print("前200字符:", clean_text[:200]) # 现在创建2-grams bigrams = create_chinese_bigram(clean_text, 2) print(f"\n前20个有意义的2-grams:") meaningful_count = 0 for i, bg in enumerate(bigrams[:100]): # 只显示有意义的组合(排除包含空白、奇怪字符的) if (len(bg) == 2 and not any(c.isspace() for c in bg) and not any(ord(c) < 0x4e00 for c in bg)): # 只显示中文字符 print(f"{meaningful_count+1}: {bg}") meaningful_count += 1 if meaningful_count >= 20: break得到:
清洗后文本长度: 259126 字符 前200字符: 家 风刮得很紧,雪片像扯破了的棉絮一样在空中飞舞,没有目的地四处飘落。左右两边墙脚各有一条白色的路,好像给中间满是水泥的石板路镶了两道宽边。 街上有行人和两人抬的轿子。他们斗不过风雪,显出了畏缩的样子。雪片愈落愈多,白茫茫地布满在天空中,向四处落下,落在伞上,落在轿顶上,落在轿夫的笠上,落在行人的脸上。 风玩弄着伞,把它吹得向四面偏倒,有一两次甚至吹得它离开了行人的手。风在 前20个有意义的2-grams: 1: 风刮 2: 刮得 3: 得很 4: 很紧 5: 紧, 6: ,雪 7: 雪片 8: 片像 9: 像扯 10: 扯破 11: 破了 12: 了的 13: 的棉 14: 棉絮 15: 絮一 16: 一样 17: 样在 18: 在空 19: 空中 20: 中飞但这是字符级的2-gram,不太符合正常的语句输出,应该改成词语级2-gram,即在原来上面的jieba基础上先洗数据再生成就好了
import re import jieba import random # 先定义清洗函数 def clean_chinese_text(text): """ 清洗中文文本,去除格式垃圾 """ # 1. 去除数字编号(如 "1"、"2:") text = re.sub(r'[0-90-9]+[::]?\s*', '', text) # 2. 去除分隔线和多余横线 text = re.sub(r'-{4,}', '', text) # 3. 将多个换行合并为1个 text = re.sub(r'\n+', '\n', text) # 4. 去除行首尾的空白字符 lines = [line.strip() for line in text.split('\n')] text = '\n'.join([line for line in lines if line]) # 保留非空行 return text # 1. 先清洗 clean_text = clean_chinese_text(text) print(f"清洗后文本长度: {len(clean_text)} 字符") print("前200字符:", clean_text[:200]) # 2. 中文分词(用清洗后的文本!) def create_chinese_ngram_model(clean_text, n=2): """使用清洗后的文本创建词语级 n-gram 模型""" # 分词 words = list(jieba.cut(clean_text)) # 过滤掉纯空白字符 words = [w for w in words if w.strip()] # 生成 n-grams ngrams = [] for i in range(len(words) - n + 1): ngram = ' '.join(words[i:i + n]) ngrams.append(ngram) return ngrams, words # 3. 重新生成 n-gram chinese_ngrams, word_list = create_chinese_ngram_model(clean_text, n=2) print(f"\n前10个有意义的词语级2-grams: {chinese_ngrams[:10]}") # 4. 中文文本生成函数(保持不变) def generate_chinese_text(ngram_model, word_list, n=2, length=30): if not ngram_model: return "" # 随机选择一个起始 n-gram start = random.choice(ngram_model) generated_words = start.split() while len(generated_words) < length: # 获取最后 n-1 个词作为上下文 context = ' '.join(generated_words[-(n-1):]) # 查找可能的下一词 possible_next = [] for ngram in ngram_model: if ngram.startswith(context + ' '): next_word = ngram.split()[-1] possible_next.append(next_word) if not possible_next: break # 随机选择一个下一个词 next_word = random.choice(possible_next) generated_words.append(next_word) # 将词语列表连接成文本 return ''.join(generated_words) # 5. 生成文本 generated = generate_chinese_text(chinese_ngrams, word_list, n=2, length=100) print("\n生成的文本:") print(generated)得到:
清洗后文本长度: 250833 字符 前200字符: 家 风刮得很紧,雪片像扯破了的棉絮一样在空中飞舞,没有目的地四处飘落。左右两边墙脚各有一条白色的路,好像给中间满是水泥的石板路镶了两道宽边。 街上有行人和两人抬的轿子。他们斗不过风雪,显出了畏缩的样子。雪片愈落愈多,白茫茫地布满在天空中,向四处落下,落在伞上,落在轿顶上,落在轿夫的笠上,落在行人的脸上。 风玩弄着伞,把它吹得向四面偏倒,有一两次甚至吹得它离开了行人的手。风在空中怒吼,声音凄厉,跟雪 前10个有意义的词语级2-grams: ['家 风', '风 刮得', '刮得 很', '很 紧', '紧 ,', ', 雪片', '雪片 像', '像 扯破', '扯破 了', '了 的'] 生成的文本: 。笑声,为那一对大眼睛,觉民看我已经变成了,觉得在房里。"'你难道我们满意为止。"汪先生,""什么关系,或者跟另一个人。""我罢,说不过做个傀儡。还是趁早休息一会儿再说,祖父是公请觉慧告诉他听见觉慧,被,乡下人不能挽救的满是很费力思索里,轻轻地走开了,不听爷爷安葬了落下去了口,觉民的身边,他的笑3.文本质量分析
可以改成3,4,5...-gram,或者用temperature的方法。
原理:
1. 从故事书里学词语搭配 ✅ 2. 随便选个开头 ✅ 3. 用"前词"找"后词" ✅ 4. 用"温度"决定选常见的还是少见的 ✅ 5. 写完足够多就停 ✅ 6. 检查有没有句号结尾 ✅代码如下:
def generate_chinese_text_improved(ngram_model, word_list, n=2, length=100, temperature=0.8): """ 改进版中文文本生成 temperature: 控制随机性,越低越保守 """ if not ngram_model: return "" # 避免以标点开头 valid_starts = [ng for ng in ngram_model if not ng[0] in ',。!?、;:'] if not valid_starts: valid_starts = ngram_model start = random.choice(valid_starts) generated_words = start.split() while len(generated_words) < length: context = ' '.join(generated_words[-(n-1):]) # 找到所有可能的下一词 next_word_candidates = {} for ngram in ngram_model: if ngram.startswith(context + ' '): next_word = ngram.split()[-1] next_word_candidates[next_word] = next_word_candidates.get(next_word, 0) + 1 if not next_word_candidates: break # 按出现频率加权随机选择(temperature调节) candidates = list(next_word_candidates.keys()) weights = [count ** (1/temperature) for count in next_word_candidates.values()] total_weight = sum(weights) probs = [w/total_weight for w in weights] next_word = random.choices(candidates, weights=probs, k=1)[0] generated_words.append(next_word) # 如果生成了句号,有一定概率停止 if next_word in '。!?' and len(generated_words) > length/2: if random.random() > 0.7: break result = ''.join(generated_words) # 后处理:确保以句号结束 if result and result[-1] not in '。!?': result += '。' return result # 使用改进版生成 generated_improved = generate_chinese_text_improved(chinese_ngrams, word_list, n=2, length=150, temperature=0.9) print("\n改进后生成的文本:") print(generated_improved)结果:
改进后生成的文本: 说:"你们看他想,立刻到新的地位,一面又有一些时候他不会演戏的滑稽戏又用温和,她又站在她真好,才有同意这句话,""我说,响个不停。他那里,觉得一切。他等他们,才深深地感动觉新突然又涂上坐下去,为什么要知道,各人的事情在父亲看见所爱的态度也曾在责备道,"大哥,小船了西装外还没有机会发泄。"琴的灵柩的挽联去了光彩,"你们快,因为你们,阶下。"众人立在檐下燃着熊熊地烧一下,我们又怎么会伤害觉民讲话,剑云才慢慢地送出来贴了片刻就拔步往外面走。客观评价:
def evaluate_generated_text(text, ngram_model, original_words): """ 评估生成文本质量的几个指标 """ from collections import Counter import math # 1. 词汇多样性(Type-Token Ratio) words = list(jieba.cut(text)) unique_words = set(words) ttr = len(unique_words) / len(words) if words else 0 # 2. 平均句子长度 sentences = [s for s in text.split('。') if s.strip()] avg_sent_len = sum(len(s) for s in sentences)/len(sentences) if sentences else 0 # 3. n-gram重复率(检查是否循环) bigrams = [text[i:i+2] for i in range(len(text)-1)] bigram_counts = Counter(bigrams) repeated_bigrams = sum(1 for count in bigram_counts.values() if count > 1) repeat_rate = repeated_bigrams / len(bigram_counts) if bigram_counts else 0 # 4. 困惑度(近似,越小越好) # 计算生成文本在训练数据中的概率 log_prob = 0 word_count = 0 for i in range(len(words)-1): bigram = f"{words[i]} {words[i+1]}" if bigram in ngram_model: # 简化计算:出现次数/总bigram数 bigram_freq = ngram_model.count(bigram) / len(ngram_model) if bigram_freq > 0: log_prob += math.log(bigram_freq) word_count += 1 perplexity = math.exp(-log_prob/word_count) if word_count > 0 else float('inf') return { "词汇多样性(TTR)": round(ttr, 3), # 0.4-0.7较好 "平均句长": round(avg_sent_len, 1), # 15-25字较好 "重复率": round(repeat_rate, 3), # <0.2较好 "困惑度": round(perplexity, 1) # 越小越好 } # 评估你的生成文本 metrics = evaluate_generated_text(generated_improved, chinese_ngrams, word_list) print("生成文本评估指标:") for k, v in metrics.items(): print(f" {k}: {v}") 生成文本评估指标: 词汇多样性(TTR): 0.68 平均句长: 42.8 重复率: 0.043 困惑度: 21094.34.改进文本质量
降低了temperature,发现有改进
生成文本评估指标: 词汇多样性(TTR): 0.679 平均句长: 17.8 重复率: 0.028 困惑度: 11919.9
垃圾邮件的处理,详情看kaggle把,累了,不想写了
文本数据 ↓ [预处理] → 清洗、分词 ↓ [特征提取] → TF-IDF转换(词→数字) ↓ [训练阶段] → 计算: 1. P(类别) - 先验概率 2. P(词|类别) - 条件概率 ↓ [预测阶段] → 对新邮件: 1. 提取特征 2. 计算 P(类别|邮件) ∝ P(类别) × ∏P(词|类别) 3. 选择概率最大的类别 ↓ 输出预测结果