问题描述:样本为所有恐龙名字,为了构建字符级语言模型来生成新的名称,你的模型将学习不同的名称模式,并随机生成新的名字。

在这里你将学习到:

  • 如何存储文本数据以便使用rnn进行处理。
  • 如何合成数据,通过每次采样预测,并将其传递给下一个rnn单元。
  • 如何构建字符级文本生成循环神经网络。
  • 为什么梯度修剪很重要?
 import numpy as np
import random
import time
import cllm_utils

1 - 问题描述

1.1 - 数据集与预处理

 # 获取名称
data = open("dinos.txt", "r").read() # 转化为小写字符
data = data.lower() # 转化为无序且不重复的元素列表
chars = list(set(data)) # 获取大小信息
data_size, vocab_size = len(data), len(chars) print(chars)
print("共计有%d个字符,唯一字符有%d个"%(data_size,vocab_size))
data='Aachenosaurus\nAardonyx\nAbdallahsaurus\...'
chars=['o', 'm', 'k', 'v', 'w', 'b', 'j', 'd', 'x', 'a', 'h', 'i',
'e', 'l', 's', 't', 'n', 'z', 'p', 'y', 'g', 'f', '\n', 'q',
'r', 'u', 'c']
共计有19909个字符,唯一字符有27个
 char_to_ix = {ch:i for i,ch in enumerate(sorted(chars))}
ix_to_char = {i:ch for i,ch in enumerate(sorted(chars))} print(char_to_ix)
print(ix_to_char)
 {'\n': 0, 'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5, 'f': 6, 
'g': 7, 'h': 8, 'i': 9, 'j': 10, 'k': 11, 'l': 12, 'm': 13,
'n': 14, 'o': 15, 'p': 16, 'q': 17, 'r': 18, 's': 19, 't': 20,
'u': 21, 'v': 22, 'w': 23, 'x': 24, 'y': 25, 'z': 26}
{0: '\n', 1: 'a', 2: 'b', 3: 'c', 4: 'd', 5: 'e', 6: 'f',
7: 'g', 8: 'h', 9: 'i', 10: 'j', 11: 'k', 12: 'l', 13: 'm',
14: 'n', 15: 'o', 16: 'p', 17: 'q', 18: 'r', 19: 's', 20: 't',
21: 'u', 22: 'v', 23: 'w', 24: 'x', 25: 'y', 26: 'z'}

1.2 - 模型回顾

模型的结构如下:

  • 初始化参数
  • 循环:
    • 前向传播计算损失
    • 反向传播计算关于损失的梯度
    • 修剪梯度以免梯度爆炸
    • 用梯度下降更新规则更新参数。
  • 返回学习后了的参数

2 - 构建模型中的模块

在这部分,我们将来构建整个模型中的两个重要的模块:

  • 梯度修剪:避免梯度爆炸
  • 取样:一种用来产生字符的技术

2.1 梯度修剪

     在这里,我们将实现在优化循环中调用的clip函数.回想一下,整个循环结构通常包括前向传播、成本计算、反向传播和参数更新。

在更新参数之前,我们将在需要时执行梯度修剪,以确保我们的梯度不是“爆炸”的.

    接下来我们将实现一个修剪函数,该函数输入一个梯度字典输出一个已经修剪过了的梯度.有很多的方法来修剪梯度,我们在这里

使用一个比较简单的方法.梯度向量的每一个元素都被限制在[−N,N]的范围,通俗的说,有一个maxValue(比如10),

如果梯度的任何值大于10,那么它将被设置为10,如果梯度的任何值小于-10,那么它将被设置为-10,如果它在-10与10之间,那么它将不变。

 def clip(gradients, maxValue):
"""
使用maxValue来修剪梯度 参数:
gradients -- 字典类型,包含了以下参数:"dWaa", "dWax", "dWya", "db", "dby"
maxValue -- 阈值,把梯度值限制在[-maxValue, maxValue]内 返回:
gradients -- 修剪后的梯度
"""
# 获取参数
dWaa, dWax, dWya, db, dby = gradients['dWaa'], gradients['dWax'], gradients['dWya'], gradients['db'], gradients['dby'] # 梯度修剪
for gradient in [dWaa, dWax, dWya, db, dby]:
np.clip(gradient, -maxValue, maxValue, out=gradient) gradients = {"dWaa": dWaa, "dWax": dWax, "dWya": dWya, "db": db, "dby": dby} return gradients

函数接受最大阈值,并返回修剪后的梯度

2.2 - 采样

 1 def sample(parameters, char_to_is, seed):
2 """
3 根据RNN输出的概率分布序列对字符序列进行采样
4
5 参数:
6 parameters -- 包含了Waa, Wax, Wya, by, b的字典
7 char_to_ix -- 字符映射到索引的字典
8 seed -- 随机种子
9
10 返回:
11 indices -- 包含采样字符索引的长度为n的列表。
12 """
13
14 # 从parameters 中获取参数
15 Waa, Wax, Wya, by, b = parameters['Waa'], parameters['Wax'], parameters['Wya'],
parameters['by'], parameters['b']
16 vocab_size = by.shape[0]
17 n_a = Waa.shape[1]
18
19 # 步骤1
20 ## 创建独热向量x
21 x = np.zeros((vocab_size,1))
22
23 ## 使用0初始化a_prev
24 a_prev = np.zeros((n_a,1))
25
26 # 创建索引的空列表,这是包含要生成的字符的索引的列表。
27 indices = []
28
29 # IDX是检测换行符的标志,我们将其初始化为-1。
30 idx = -1
31
32 # 循环遍历时间步骤t。在每个时间步中,从概率分布中抽取一个字符,
33 # 并将其索引附加到“indices”上,如果我们达到50个字符,
34 #(我们应该不太可能有一个训练好的模型),我们将停止循环,这有助于调试并防止进入无限循环
35 counter = 0
36 newline_character = char_to_ix["\n"]
37
38 while (idx != newline_character and counter < 50):
39 # 步骤2:使用公式1、2、3进行前向传播
40 a = np.tanh(np.dot(Wax, x) + np.dot(Waa, a_prev) + b)
41 z = np.dot(Wya, a) + by
42 y = cllm_utils.softmax(z)
43
44 # 设定随机种子
45 np.random.seed(counter + seed)
46
47 # 步骤3:从概率分布y中抽取词汇表中字符的索引
48 idx = np.random.choice(list(range(vocab_size)), p=y.ravel())
49
50 # 添加到索引中
51 indices.append(idx)
52
53 # 步骤4:将输入字符重写为与采样索引对应的字符。
54 x = np.zeros((vocab_size,1))
55 x[idx] = 1
56
57 # 更新a_prev为a
58 a_prev = a
59
60 # 累加器
61 seed += 1
62 counter +=1
63
64 if(counter == 50):
65 indices.append(char_to_ix["\n"])
66
67 return indices

3 - 构建语言模型

3.1 - 梯度下降

 在这里,我们将实现一个执行随机梯度下降的一个步骤的函数(带有梯度修剪)。我们将一次训练一个样本,所以优化算法将是随机梯度下降,这里是RNN的一个通用的优化循环的步骤:

  • 前向传播计算损失
  • 反向传播计算关于参数的梯度损失
  • 修剪梯度
  • 使用梯度下降更新参数

 我们来实现这一优化过程(单步随机梯度下降),这里我们提供了一些函数:

  # 示例,可参照上一篇博客RNN的前向后向传播。
def rnn_forward(X, Y, a_prev, parameters):
"""
通过RNN进行前向传播,计算交叉熵损失。 它返回损失的值以及存储在反向传播中使用的“缓存”值。
"""
....
return loss, cache def rnn_backward(X, Y, parameters, cache):
"""
通过时间进行反向传播,计算相对于参数的梯度损失。它还返回所有隐藏的状态
"""
...
return gradients, a def update_parameters(parameters, gradients, learning_rate):
"""
Updates parameters using the Gradient Descent Update Rule
"""
...
return parameters
def optimize(X, Y, a_prev, parameters, learning_rate = 0.01):
"""
执行训练模型的单步优化。 参数:
X -- 整数列表,其中每个整数映射到词汇表中的字符。
Y -- 整数列表,与X完全相同,但向左移动了一个索引。
a_prev -- 上一个隐藏状态
parameters -- 字典,包含了以下参数:
Wax -- 权重矩阵乘以输入,维度为(n_a, n_x)
Waa -- 权重矩阵乘以隐藏状态,维度为(n_a, n_a)
Wya -- 隐藏状态与输出相关的权重矩阵,维度为(n_y, n_a)
b -- 偏置,维度为(n_a, 1)
by -- 隐藏状态与输出相关的权重偏置,维度为(n_y, 1)
learning_rate -- 模型学习的速率 返回:
loss -- 损失函数的值(交叉熵损失)
gradients -- 字典,包含了以下参数:
dWax -- 输入到隐藏的权值的梯度,维度为(n_a, n_x)
dWaa -- 隐藏到隐藏的权值的梯度,维度为(n_a, n_a)
dWya -- 隐藏到输出的权值的梯度,维度为(n_y, n_a)
db -- 偏置的梯度,维度为(n_a, 1)
dby -- 输出偏置向量的梯度,维度为(n_y, 1)
a[len(X)-1] -- 最后的隐藏状态,维度为(n_a, 1)
""" # 前向传播
loss, cache = cllm_utils.rnn_forward(X, Y, a_prev, parameters) # 反向传播
gradients, a = cllm_utils.rnn_backward(X, Y, parameters, cache) # 梯度修剪,[-5 , 5]
gradients = clip(gradients,5) # 更新参数
parameters = cllm_utils.update_parameters(parameters,gradients,learning_rate) return loss, gradients, a[len(X)-1]

给定恐龙名称的数据集,我们使用数据集的每一行(一个名称)作为一个训练样本。每100步随机梯度下降,你将抽样10个随机选择的名字,看看算法是怎么做的。

3.2 - 训练模型

记住要打乱数据集,以便随机梯度下降以随机顺序访问样本。当examples[index]包含一个恐龙名称(String)时,为了创建一个样本(X,Y),你可以使用这个:

 index = j % len(examples)
X = [None] + [char_to_ix[ch] for ch in examples[index]]
Y = X[1:] + [char_to_ix["\n"]]
 def model(data, ix_to_char, char_to_ix, num_iterations=3500,
n_a=50, dino_names=7,vocab_size=27):
"""
训练模型并生成恐龙名字 参数:
data -- 语料库
ix_to_char -- 索引映射字符字典
char_to_ix -- 字符映射索引字典
num_iterations -- 迭代次数
n_a -- RNN单元数量
dino_names -- 每次迭代中采样的数量
vocab_size -- 在文本中的唯一字符的数量 返回:
parameters -- 学习后了的参数
""" # 从vocab_size中获取n_x、n_y
n_x, n_y = vocab_size, vocab_size # 初始化参数
parameters = cllm_utils.initialize_parameters(n_a, n_x, n_y) # 初始化损失
loss = cllm_utils.get_initial_loss(vocab_size, dino_names) # 构建恐龙名称列表
with open("dinos.txt") as f:
examples = f.readlines()
examples = [x.lower().strip() for x in examples] # 打乱全部的恐龙名称
np.random.seed(0)
np.random.shuffle(examples) # 初始化LSTM隐藏状态
a_prev = np.zeros((n_a,1)) # 循环
for j in range(num_iterations):
# 定义一个训练样本
index = j % len(examples)
X = [None] + [char_to_ix[ch] for ch in examples[index]]
Y = X[1:] + [char_to_ix["\n"]] # 执行单步优化:前向传播 -> 反向传播 -> 梯度修剪 -> 更新参数
# 选择学习率为0.01
curr_loss, gradients, a_prev = optimize(X, Y, a_prev, parameters) # 使用延迟来保持损失平滑,这是为了加速训练。
loss = cllm_utils.smooth(loss, curr_loss) # 每2000次迭代,通过sample()生成“\n”字符,检查模型是否学习正确
if j % 2000 == 0:
print("第" + str(j+1) + "次迭代,损失值为:" + str(loss)) seed = 0
for name in range(dino_names):
# 采样
sampled_indices = sample(parameters, char_to_ix, seed)
cllm_utils.print_sample(sampled_indices, ix_to_char) # 为了得到相同的效果,随机种子+1
seed += 1 print("\n")
return parameters

比如说某恐龙名字叫 zzh

那么X = ['0','z','z','h']

Y = ['z','z','h','\n']

需要注意的是我们使用了

index= j % len(examples),

其中= 1....num_iterations,

为了确保examples[index]总是有效的

(index小于len(examples)),

rnn_forward()会将X的第一个值None解释为

x<0>=0向量.

此外,为了确保Y等于X,会向左移动一步,

并添加一个附加的“\n”以表示恐龙名称的结束。

 #开始时间
start_time = time.clock() #开始训练
parameters = model(data, ix_to_char, char_to_ix, num_iterations=3500) #结束时间
end_time = time.clock() #计算时差
minium = end_time - start_time print("执行了:" + str(int(minium / 60)) + "分" + str(int(minium%60)) + "秒")

结果如下:

第1次迭代,

损失值为:23.0873360855
Nkzxwtdmfqoeyhsqwasjkjvu
Kneb
Kzxwtdmfqoeyhsqwasjkjvu
Neb
Zxwtdmfqoeyhsqwasjkjvu
Eb
Xwtdmfqoeyhsqwasjkjvu

第2001次迭代,

损失值为:27.8841604914
Liusskeomnolxeros
Hmdaairus
Hytroligoraurus
Lecalosapaus
Xusicikoraurus
Abalpsamantisaurus
Tpraneronxeros

以上是自己定义参数来实现字符语言模型,下面用keras实现。

#获取恐龙的名称
data = open('dinos.txt','r').read()
data = data.lower() chars = list(set(data)) data_size,vocab_size = len(data),len(chars) print(chars)
print("共计有%d个字符,唯一字符有%d个"%(data_size,vocab_size))

['r', 'p', 'j', 'i', 't', 'z', 'q', 'o', 'd',
'x', 's', 'v', 'e', 'l', 'g', 'k', 'n', 'm',
'c', 'b', 'f', 'y', 'w', 'u', 'h', 'a', '\n']
共计有19909个字符,唯一字符有27个

kl_name = open('dinos.txt','r').read().lower()
# kl_name =kl_name.lower().split('\n')
# len(kl_name.split('\n'))
print(kl_name[:50])
 

char_to_ix = {ch:i for i,ch in enumerate(sorted(chars))}
ix_to_char = {i:ch for i,ch in enumerate(sorted(chars))} print(char_to_ix)
print(ix_to_char)

{'\n': 0, 'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5, 
'f': 6, 'g': 7, 'h': 8, 'i': 9, 'j': 10, 'k': 11,
'l': 12, 'm': 13, 'n': 14, 'o': 15, 'p': 16,
'q': 17, 'r': 18, 's': 19, 't': 20, 'u': 21,
'v': 22, 'w': 23, 'x': 24, 'y': 25, 'z': 26}
{0: '\n', 1: 'a', 2: 'b', 3: 'c', 4: 'd', 5: 'e',
6: 'f', 7: 'g', 8: 'h', 9: 'i', 10: 'j', 11: 'k',
12: 'l', 13: 'm', 14: 'n', 15: 'o', 16: 'p',
17: 'q', 18: 'r', 19: 's', 20: 't', 21: 'u',
22: 'v', 23: 'w', 24: 'x', 25: 'y', 26: 'z'}

#将字符序列向量化
max_len = 8 names = []
next_chars = [] for i in range(0,len(kl_name)-maxlen):
names.append(kl_name[i:i+max_len])
next_chars.append(kl_name[i+max_len])
#每句话之后的下一个字符,相当于是label x = np.zeros((len(names),maxlen,len(char_to_ix)))
y = np.zeros((len(names),len(char_to_ix))) for i,name in enumerate(names):
for j,char in enumerate(name):
x[i,j,char_to_ix[char]] = 1
y[i,char_to_ix[next_chars[i]]]=1 print('x.shape',x.shape)
print('y.shape',y.shape)
print(names[:4])
print(next_chars[:4])
x.shape (19879, 30, 27)
y.shape (19879, 27)
['aachenos', 'achenosa', 'chenosau', 'henosaur']
['a', 'u', 'r', 'u']

x是从原字符串中,每max_len个字符生成的样本

y是每个样本的后一个字符

比如说原字符串为=''abcdefghijklmn',max_len=8

x = ['abcdefgh','bcdefghi','cdefghij',...]

y = ['i','j','k',...]

#构建用于预测下一个字符的单层LSTM模型
from keras.layers import SimpleRNN,Dense
from keras.models import Sequential
model = Sequential()
model.add(SimpleRNN(128,input_shape=(maxlen,len(char_to_ix))))
model.add(Dense(len(char_to_ix),activation='softmax'))
model.summary()
 

注意:输入model里面的input的size是不包括所有样本的,

也就是说只有一个样本的大小(时间步,oe-hot的长度),

在fit的时候x是包含所有样本的x.shape(样本个数,样本的

长度,每个字符one-hot的长度)

#模型编译配置
import keras
optimizers = keras.optimizers.RMSprop(lr=0.01)
model.compile(optimizer='rmsprop',
loss = 'categorical_crossentropy',
metrics = ['acc']) #目标是经过one-hot编码的,所以训练模型需要使用categorical_crossentropy作为损失
 
   

#给定模型预测、采样下一个字符的函数
def sample(preds,temperature=0.1):
preds = np.asarray(preds).astype('float64')
preds = np.log(preds) / temperature
exp_preds = np.exp(preds)
preds = exp_preds / np.sum(exp_preds)
probas = np.random.multinomial(1,preds,1)
return np.argmax(probas)

 每次predict之后,得到一个softmax之后的向量,该选取

哪个单词作为label呢?

(1)贪婪采样:每次都选可能性最大的下一个字符,但这种方法会

得到重复的、可预测的字符串

(2)随机采样:控制随机性的大小-->softmax温度

更高的温度得到的熵是更大的采样分布,会生成更加出人意料、

更加无结构的生成数据;更低的温度对应更小的随机性,以及更

加可预测的生成数据。

import sys
import numpy as np
#文本生成循环
epochs = 6
for epoch in range(1,epochs):#每次循环生成10个字符(只保留最新的10个),循环6次
print('epoch',epoch)
model.fit(x,y,batch_size=128,epochs=1) #随机选取一个文本片段
start_index = np.random.randint(0,len(names)-1)
generated_text = names[start_index]
print('--- Generating with seed:"' + generated_text[1:] + '"')
for temperature in [0.2,0.5,1.0,1.2]:
print("第"+str(epoch)+"次,temperature="+str(temperature)+
"生成的文本为"+generated_text) #生成10个字符
for i in range(10):
sampled = np.zeros((1,maxlen,len(char_to_ix)))
for j,char in enumerate(generated_text):
sampled[0,j,char_to_ix[char]] = 1
          #将生成的10个字符转成one-hot的形式
preds = model.predict(sampled,verbose=0)[0]
        #预测的结果 predict的shape为(1,27) next_index = sample(preds=preds,temperature=temperature)
        #采样
next_char = ix_to_char[next_index] generated_text += next_char
generated_text = generated_text[1:]
       #更新generated_text每轮只留最新的后0个字符

epoch 1
Epoch 1/1
19879/19879 [==============================] - 5s
265us/step - loss: 1.3650 - acc: 0.5864
--- Generating with seed:"amosaur"
第1次,temperature=0.2生成的文本为hamosaur
(1, 27)
第1次,temperature=0.5生成的文本为hamosaur
(1, 27)
第1次,temperature=1.0生成的文本为hamosaur
(1, 27)
第1次,temperature=1.2生成的文本为hamosaur
(1, 27)
epoch 2
Epoch 1/1
19879/19879 [==============================] - 5s
273us/step - loss: 1.3569 - acc: 0.5858
--- Generating with seed:"nimanta"
第2次,temperature=0.2生成的文本为animanta
(1, 27)
第2次,temperature=0.5生成的文本为animanta
(1, 27)
第2次,temperature=1.0生成的文本为animanta
(1, 27)
第2次,temperature=1.2生成的文本为animanta
(1, 27)

参考文献:

1.【用Keras开发字符级神经网络语言模型】

2.【对于LSTM输入层、隐含层及输出层参数的个人理解】

3.【keras的主要模块介绍】

RNN实现字符级语言模型 - 恐龙岛(自己写RNN前向后向版本+keras版本)的更多相关文章

  1. tf.contrib.rnn.core_rnn_cell.BasicLSTMCell should be replaced by tf.contrib.rnn.BasicLSTMCell.

    For Tensorflow 1.2 and Keras 2.0, the line tf.contrib.rnn.core_rnn_cell.BasicLSTMCell should be repl ...

  2. 自己动手写RNN

    说的再好,也不如实际行动,今天手写了一个RNN,没有使用Numpy库,自己写的矩阵运算方法,由于这也只是个学习用的demo,所以矩阵运算那一部分写的比较丑陋,见笑了. import com.mylea ...

  3. 代码实现:获取一个文本上每个字符出现的次数,将结果写在times.txt上

    package com.loaderman.test; import java.io.BufferedReader; import java.io.BufferedWriter; import jav ...

  4. 字符输出流_Writer类&FileWriter类介绍和字符输出流的基本使用_写出单个字符到文件

    java.io.Writer:字符输出流,是所有字符输出流的最顶层的父类,是一个抽象类 共性的成员方法: - void write(int c) 写入单个字符 - void write(char[] ...

  5. 字符输出流_Writer类&FileWrite类介绍和字符输出流的基本使用_写出单个字符到文件

    字符输出流_Writer类&FileWrite类介绍 java.io.Writer:字符输出流,是所有字符输出流的最顶层的父类,是一个抽象类 共性抽象方法: void write(int c) ...

  6. Pytorch系列教程-使用字符级RNN生成姓名

    前言 本系列教程为pytorch官网文档翻译.本文对应官网地址:https://pytorch.org/tutorials/intermediate/char_rnn_generation_tutor ...

  7. Pytorch系列教程-使用字符级RNN对姓名进行分类

    前言 本系列教程为pytorch官网文档翻译.本文对应官网地址:https://pytorch.org/tutorials/intermediate/char_rnn_classification_t ...

  8. RNN 通过字符语言模型 理解BPTT

    链接:https://github.com/karpathy/char-rnn http://karpathy.github.io/2015/05/21/rnn-effectiveness/ http ...

  9. 学习笔记TF021:预测编码、字符级语言建模、ArXiv摘要

    序列标注(sequence labelling),输入序列每一帧预测一个类别.OCR(Optical Character Recognition 光学字符识别). MIT口语系统研究组Rob Kass ...

随机推荐

  1. [PHP] Compile an extension on Windows

    https://wiki.php.net/internals/windows/stepbystepbuildhttp://blog.benoitblanchon.fr/build-php-extens ...

  2. Sencha Touch 实战开发培训 视频教程 第二期 第三节

    2014.4.11晚上8:10分开课. 本节课耗时一小时以上. 本期培训一共八节,前两节免费,后面的课程需要付费才可以观看. 本节内容:             本地储存.扩展按钮控件.微博分享 实现 ...

  3. Yet another way to manage your NHibernate ISessionFactory

    So here is my current UnitOfWork implementation.  This one makes use of the somewhat new current_ses ...

  4. 解决使用Foxmail客户端软件不能收取腾讯企业邮箱的全部邮件

    一般说来,使用Foxmail客户端软件收取邮箱时,需要作如下几步: 1.进入邮箱web界面授权开启POP3/SMTP服务.IMAP/SMTP等服务 2.在邮箱web界面配置收取选项,可选择收取全部邮件 ...

  5. mysql概要(四)order by ,limit ,group by和聚合函数的特点,子查询

    1.order by 默认按升序排列(asc/desc),多字段排序 order by 字段 排序方式,字段2 排序方式,..: 在分组排序中,排序是对分组后的结果进行排序,而不是在组中进行排序. s ...

  6. Spring Framework框架容器核心源码逐步剖析

    目录 构建Spring环境 Spring 版本 5.1.3.RELEASE 测试类 Spring 配置文件 测试方法Main 快速进入Debug查看IOC容器构建源码 Spring IOC源码步骤分析 ...

  7. Python的一个命名空间冲突,关于from-import机制

    from os import * #import os def foo(): a = listdir("trainingDigits") b = open("traini ...

  8. DDOS hulk,rudy

    HULK (HTTP Unbearable Load King) HULK HULK是另一个DOS攻击工具,这个工具使用UserAgent的伪造,来避免攻击检测,可以通过启动500线程对目标发起高频率 ...

  9. Iwconfig/aircrack-ng

    BT5 aircrack-ng破解无线密码(wpa/wep) - 星明月稀 - 博客频道 - CSDN.NET             BT5 aircrack-ng破解无线密码(wpa/wep) - ...

  10. iOS-数据缓存(转载)

    一.关于同一个URL的多次请求 有时候,对同一个URL请求多次,返回的数据可能都是一样的,比如服务器上的某张图片,无论下载多少次,返回的数据都是一样的. 上面的情况会造成以下问题 (1)用户流量的浪费 ...