简介:基于深度学习的语音识别课程设计源码包,面向计算机相关专业在校学生、教师及企业开发者,尤其适合作为课程作业、期末大作业或毕业设计的参考项目。包内提供可直接运行的完整工程,核心基于CRNN与CTC的识别思路,包含数据预处理脚本、自定义网络层、模型训练与推理主程序,并配有结构示意图和说明文档,便于理解整体流程与二次扩展。压缩包共13个文件,包含3个Python源码、多个XML/IML工程配置文件、Markdown说明文件、一张网络结构图以及编译缓存文件,整体仅1.29MB,十分轻量易部署。当前已有743人学习下载,代码经过运行测试,适合有一定Python基础并希望快速上手深度学习语音识别项目的读者参考借鉴,可在此基础上修改增加功能,服务于课程设计、初期项目演示等场景。
1. 课程设计里的语音识别:这套 CRNN+CTC 源码真正能跑通的东西
如果你下载过几个深度学习语音识别的课程设计源码,一定遇到过这种情况:压缩包打开一堆文件,却不知道该先运行哪个,最后只能看 README 跑 Demo。这份基于深度学习的语音识别 python 源码不太一样,它由 CRNNCTC.py、TFDataUtils.py、CustomLayers.py 三个核心文件加一份说明文档组成,是一个典型的 CRNN + CTC 端到端识别工程。输入是 wav 音频切片后的梅尔特征,输出直接是汉字或字符序列,绕开了传统语音识别里 HMM 和强制对齐那套前置知识。对于计科、人工智能、大数据方向的同学来说,适合做期末大作业、课程设计或者毕设开题演示。基础薄弱的可以直接把它当黑匣子先跑通;有点基础的同学则可以改字符表、换网络层,做成自己的一套东西。
2. 读懂 CRNNCTC.py 的模型骨架:卷积特征提取与双向 GRU 的配合
语音识别课程设计的核心难点不在于 Python 语法,而在于“音频帧数”和“标签长度”是不对齐的。CRNNCTC.py 要解决的正是这个问题,它把模型拆成卷积特征提取、循环神经网络建模、CTC 损失对齐三个部分。下面从原理讲起,再落到这个文件里最常见的结构。
2.1 从音频特征到稀疏张量:CRNN+CTC 为什么能端到端识别
一段 1 秒的音频以 16kHz 采样,按 10ms 帧移切出来约 100 帧,而对应的文字可能只有 3 个汉字。传统 HMM 方案需要先做音素对齐,把每一帧对应到某个音素上,这个过程既依赖语言模型又依赖专家知识。CTC 解决这个问题的方式是引入一个空白符号,允许某一帧输出空白,也允许连续重复同一个字符,最终在解码阶段把重复和空白合并,得到原始标签序列。模型不需要知道字在哪一帧结束,只需要在整体上最大化正确序列的概率,这就是 CTC 的端到端思想。
CRNN 里 CNN 负责从梅尔谱中提取局部声学模式,并通过池化压缩时间分辨率;RNN 用双向 GRU 学习上下文关系,让前向和后向信息共同决定当前帧的输出。相比纯 RNN,CRNN 的收敛速度更快;相比纯 CNN,它又能捕捉时序依赖。课程设计里常见的词表大小从几十到几百不等,用这套结构训练一小段中文命令词识别,效果和训练速度都很合适。
2.2 CRNNCTC.py 关键结构拆解:Conv2D、批归一化、双向 GRU 与 CTC loss
以这类工程最常见的写法为例,CRNNCTC.py 里的模型通常先是几层卷积,每层后面跟着批归一化和最大池化。卷积核多选 3×3,池化步长多选 2×2,这意味着每经过一层池化,时间维长度近似减半。下面这段代码是典型的 CRNN 前端:
import tensorflow as tf def build_crnn_frontend(inputs, num_classes, rnn_units=128): # inputs 形状: [batch, time, freq, 1] net = tf.keras.layers.Conv2D( filters=32, kernel_size=3, strides=(1, 1), padding='same', activation='relu', name='conv1')(inputs) net = tf.keras.layers.BatchNormalization(name='bn1')(net) net = tf.keras.layers.MaxPooling2D( pool_size=(2, 2), strides=(2, 2), name='pool1')(net) net = tf.keras.layers.Conv2D( filters=64, kernel_size=3, strides=(1, 1), padding='same', activation='relu', name='conv2')(net) net = tf.keras.layers.BatchNormalization(name='bn2')(net) net = tf.keras.layers.MaxPooling2D( pool_size=(2, 2), strides=(2, 2), name='pool2')(net) # 把高和融合进通道,只保留时间维 shape = net.get_shape().as_list() net = tf.reshape(net, [tf.shape(net)[0], shape[1], -1]) return net逻辑说明:Conv2D 先提取局部声学特征,BatchNormalization 让每一层输入分布稳定,避免梯度消失和初始 loss 太大。两层 MaxPooling 把时间维压缩约 4 倍,也就是说原始 100 帧音频,进入 RNN 时大约只剩 25 个时间步。最后用 reshape 把频率维和通道维合并,得到形状为[batch, time, merged_dim]的序列,这样才能送给 GRU 层。
参数说明:filters=32/64 是课程设计里最常见的小网络配置,显存不够时可以减到 16/32。rnn_units=128 控制双向 GRU 的隐层维度,词表大或者句子长时可以调到 256。需要注意最后 reshape 时使用了shape[1],如果前面池化步长改动过,这里的 time 维要跟着算清楚,否则运行时报维度错误。
双向 GRU 部分和 CTC loss 通常这样组织:
def build_rnn_and_loss(net, seq_len, sparse_labels, num_classes, rnn_units=128): outputs, _ = tf.compat.v1.nn.bidirectional_dynamic_rnn( cell_fw=tf.compat.v1.nn.rnn_cell.GRUCell(rnn_units), cell_bw=tf.compat.v1.nn.rnn_cell.GRUCell(rnn_units), inputs=net, sequence_length=seq_len, dtype=tf.float32 ) outputs = tf.concat(outputs, axis=-1) # [batch, time, 2*rnn_units] logits = tf.keras.layers.Dense(num_classes)(outputs) # [batch, time, num_classes] logits = tf.transpose(logits, [1, 0, 2]) # CTC 要求 time-major loss = tf.nn.ctc_loss( labels=sparse_labels, inputs=logits, sequence_length=seq_len ) return tf.reduce_mean(loss), logits逻辑说明:bidirectional_dynamic_rnn里sequence_length必须传实际有效时间步,不能全填最大值,否则 GRU 对 padding 部分也会计算隐状态,导致 CTC 对齐错乱。解码时同样要传真实的 seq_len。Dense(num_classes)把每个时间步映射成词表大小的 logits,注意这里输出没有 softmax,CTC loss 内部会做 softmax 归一化。最后把 logits 转成[time, batch, num_classes],这是 TensorFlow 历史遗留的约定,漏掉 transpose 会直接报 shape 错误。
参数说明:num_classes指的是字符表大小,注意是否包含空白符。如果你的字符表只有 63 个字,那么num_classes通常设 64,其中 0 号索引留给空白,或者单独指定blank_index。不同工程写法不同,建议训练前打印一下sparse_labels的值,确认空白索引没有被字符占用。
2.3 训练数据流与 batch:时序长度怎么对齐、标签怎么传
CRNNCTC.py 训练时,输入占位符一般是三个:特征、有效长度、稀疏标签。特征和长度在 TFDataUtils.py 里已经生成,训练循环里只需要组织 feed_dict。常见的占位符定义如下:
X = tf.compat.v1.placeholder(tf.float32, [None, None, None, 1], name='X') seq_len = tf.compat.v1.placeholder(tf.int32, [None], name='seq_len') sparse_labels = tf.compat.v1.sparse_placeholder(tf.int32, name='sparse_labels')逻辑说明:X 的四个维度分别是 batch、time、frequency、channel。time 维是变长的,所以设为 None,每个 batch 内的样本会 pad 到同一长度。seq_len 存放每个样本压缩后的有效时间步,这个值必须和模型实际输出时间步一致,比如原始 100 帧经过两层 stride=2 的池化后,有效长度大约是 25。sparse_labels 是稀疏张量,用于描述不同长度的标签序列,它由 indices、values、dense_shape 三部分组成,避免把标签 pad 成固定长度矩阵造成浪费。
训练时通常这样迭代:
for epoch in range(epochs): for batch_x, batch_seq_len, batch_sparse_labels in epoch_iterator: _, cost_val = sess.run( [train_op, cost], feed_dict={ X: batch_x, seq_len: batch_seq_len, sparse_labels: batch_sparse_labels } )逻辑说明:epoch_iterator来自 TFDataUtils.py 的数据读取函数,它会按 batch 切分特征和标签,并把seq_len减半后传给模型。很多新手在这里出错,以为 seq_len 是原始音频帧数,实际上必须传入经过池化压缩后的长度。建议在模型构建时用一个函数统一计算压缩倍率,不要手写死数字。
3. TFDataUtils.py 与 CustomLayers.py:把 wav 变成模型能吃的数据
模型结构看懂之后,下一步就是把音频转成符合输入的张量。TFDataUtils.py 负责特征提取和标签编码,CustomLayers.py 负责兼容不同版本的 TensorFlow 层。这两块才是课程设计里真正容易被卡住的地方。
3.1 从 wav 到梅尔谱:预加重、分帧、加窗、FBank
课程设计用的音频和语音助手不一样,不需要太大的特征维度。常见做法是用 16kHz 采样率、单声道、40 维 FBank 特征。FBank 比 MFCC 少一步离散余弦变换,神经网络自己能学会那部分去相关,所以在深度学习语音识别里更常用。一个典型的特征提取流程如下:
import numpy as np import scipy.io.wavfile as wavfile def read_wav_to_signal(path, target_sr=16000): sr, signal = wavfile.read(path) if signal.ndim > 1: signal = signal.mean(axis=1) # 多声道转单声道 if sr != target_sr: # 重采样,课程设计里可以直接线性插值,或者用 librosa signal = resample(signal, sr, target_sr) if signal.dtype != np.float32: signal = signal.astype(np.float32) / 32768.0 # int16 转 float return signal逻辑说明:wavfile.read 会同时返回采样率和数值数组。很多录音软件采出来是双声道,直接送入模型会得到错误 shape,所以要先求平均转单声道。整型转浮点并归一化到 [-1, 1] 是为了与模型训练时数值分布一致,如果不做这一步,后面算 log 谱时会溢出。
参数说明:target_sr=16000 是语音识别最常见的采样率。如果你的音频本身是 8kHz 电话录音,建议先重采样到 16kHz,否则特征和模型训练时的分布完全不匹配。resample函数可以自己写线性插值,也可以直接librosa.load(path, sr=16000, mono=True)一步到位,但注意 librosa 返回的已经是 float,不需要再除以 32768。
分帧和加窗的代码在 TFDataUtils.py 里通常长这样:
def get_fbank(signal, sample_rate=16000, n_mel=40): # 预加重 signal = np.append(signal[0], signal[1:] - 0.97 * signal[:-1]) frame_len = int(0.025 * sample_rate) # 25ms frame_step = int(0.010 * sample_rate) # 10ms frames = framing(signal, frame_len, frame_step) frames *= np.hamming(frame_len) power_spec = np.abs(np.fft.rfft(frames, n=512)) ** 2 mel_filter = make_mel_filter(sample_rate, n_fft=512, n_mel=n_mel) fbank = np.dot(power_spec, mel_filter.T) fbank = np.log(fbank + 1e-10) return fbank.astype(np.float32)逻辑说明:预加重系数 0.97 用来提升高频分量,因为语音在高频段能量衰减快。25ms 帧长、10ms 帧移是语音识别标准配置,能兼顾频率分辨率和时间分辨率。加 Hamming 窗是为了减轻频谱泄漏。最后取 log 是为了压缩动态范围,符合人耳对声音响度的感知。
参数说明:n_mel=40是课程设计里性价比最高的配置,进阶同学可以改成 80 或 128,FBank 维数上升后模型训练会变慢但可能有小幅度提升。n_fft=512在 16kHz 下频率分辨率约 31Hz,足够覆盖 300Hz 到 8kHz 的语音主能量区。这里fbank的返回值形状是[time, n_mel],后续送入模型前还要加一维 channel。
3.2 标签编码与稀疏张量:CTC 的标签序列怎么转成 indices
FP 特征转换完成后,标签也要从字符串转成整数序列。TFDataUtils.py 里通常会维护一个字符表,比如“abcdefghijklmnopqrstuvwxyz_”,然后给每个字符分配一个索引。生成稀疏张量的标准写法是:
def sparse_tuple_from(sequences, dtype=np.int32): indices = [] values = [] for batch_idx, seq in enumerate(sequences): for time_idx, token in enumerate(seq): indices.append([batch_idx, time_idx]) values.append(token) dense_shape = [len(sequences), max(len(s) for s in sequences)] return (np.array(indices, dtype=dtype), np.array(values, dtype=dtype), np.array(dense_shape, dtype=dtype))逻辑说明:CTC 接收的 labels 是稀疏张量,indices 里第一维是 batch 内第几条数据,第二维是这条数据内部的第几个字符。values 就是字符对应的整数编号。dense_shape 表示如果强行用密集矩阵存储,每个 batch 的宽度取该 batch 内最长标签长度。这样不同长度的标签可以放在同一个 batch 里,而无需 padding 成完全一致的矩阵。
参数说明:dense_shape 的第二个值在不同实现里有差异。有些工程会固定设成最大标签长度,有些会设成 batch 内动态最大长度,两者都能训练,但固定值会占用更多内存。如果你的ctc_loss一直报 label 长度超过输入时间步,请检查 dense_shape 是否填得过大。
3.3 CustomLayers 的封装:TensorFlow 版本兼容与 Lambda 层避坑
很多同学把源码下载下来后第一件事是跑pip install tensorflow,结果装的是 TF 2.x,然后 CRNNCTC.py 里tf.compat.v1.nn.bidirectional_dynamic_rnn还能用,但 CustomLayers.py 里某些自定义层报错。这个问题非常普遍。CustomLayers.py 存在的意义就是把不同版本 TensorFlow 的差异隔离开来,常见的做法是用tf.keras.layers.Layer封装一个双向 GRU:
class BidirectionalGRU(tf.keras.layers.Layer): def __init__(self, units): super().__init__() self.gru_fw = tf.keras.layers.GRU(units, return_sequences=True) self.gru_bw = tf.keras.layers.GRU(units, return_sequences=True, go_backwards=True) def call(self, inputs, mask=None): out_fw = self.gru_fw(inputs, mask=mask) out_bw = self.gru_bw(inputs, mask=mask) out_bw = tf.reverse(out_bw, axis=[1]) return tf.concat([out_fw, out_bw], axis=-1)逻辑说明:用 Keras 的 GRU 层替代tf.nn.rnn_cell.GRUCell,天然兼容 TF 2.x,不需要compat.v1。go_backwards=True让反向 GRU 从序列尾部往头部读,tf.reverse再把输出倒回正序,这样正向和反向输出在时间轴上对齐,最后 concat 成[batch, time, 2*units]。在 call 里传 mask 可以屏蔽 padding 部分对 GRU 状态的影响,比手写 sequence_length 更省心。
参数说明:units 要和 CRNNCTC.py 里的 rnn_units 保持一致。这个层在 TF 2.x 下可以直接被model.compile调用,但如果原工程还是 sess.run 式的训练循环,则需要把 layer 的 call 结果接到占位符后面。CustomLayers.py 里的其它封装,比如对输入做tf.transpose和tf.expand_dims的 Lambda 层,大多也是为了解决 TF 版本间 API 改名的问题;遇到报错时优先看这个文件里的注释和改动记录。
4. 避坑:课程设计跑起来之前必须检查的五个地方
我拿到这套源码时,第一件事不是看模型,而是先列了一个排查清单。下面五条是我见过最多的翻车点,按“现象、原因、解决”的方式写出来,你对照检查能省下至少一个下午。
4.1 现象:import CustomLayers 报错,提示没有 GRUCell 或 contrib
很多同学在 TF 2.x 下运行,报错信息是module 'tensorflow' has no attribute 'nn'或者tf.contrib不存在。原因是原工程很可能基于 TF 1.x 写的,TF 2.0 移除了一大批高层 API。
解决方式:优先把 CRNNCTC.py 里所有tf.nn.rnn_cell.GRUCell改成tf.compat.v1.nn.rnn_cell.GRUCell,同时把tf.nn.bidirectional_dynamic_rnn补齐compat.v1前缀。如果改动后发现 CustomLayers.py 仍然报错,就直接用上面 3.3 里的 Keras 层替换整个双向 GRU 块,避免继续和旧 API 纠缠。改完以后先跑一个 10 个 batch 的训练,确认不再报错再全量训练。
4.2 现象:训练时 loss 不下降,或者一开始就 NaN
如果训练几千步 loss 始终在 5.0 以上不动,大概率是特征和标签对不上。常见原因有两个:一是音频被读成了双声道或非 16kHz,特征分布和模型预期不一致;二是标签稀疏张量的 dense_shape 填得比实际最大标签长度大太多,导致 CTC 在计算时出现大量越界。NaN 通常来自 log 里遇到负数或零,比如对全零的静音段做np.log,或者学习率设成 0.01 以上。
解决方式:训练前打印一个 batch 的 X 和 labels,确认 X 的数值范围大致在 [-5, 15] 之间,labels 的最大值不超过 num_classes。把优化器换成 Adam,学习率降到 1e-3 以下,多数课程设计工程用 1e-3 起步是合理的,但遇到 NaN 就立刻降一半。
4.3 现象:预测结果全是空白,或者输出一堆重复字符
模型训练后识别出来要么是空字符串,要么是“你你你好好好”。CTC 解码时如果 blank 索引设置错,会把所有字符当成空白丢掉;如果解码后没有合并重复,中文多字词会出现大量叠字。
解决方式:检查ctc_loss里的blank_index和字符表的对齐关系。常见约定有两种:空白放在字符表末尾(索引 num_classes-1),或者放在索引 0。如果源码在 loss 里用了blank_index=0,那 TFDataUtils.py 生成的标签就绝对不能出现 0。另外,使用tf.nn.ctc_greedy_decoder时,它本身会合并重复和删除空白,但如果你在这个基础上又手动合并一次,就会把真正的叠词(比如“谢谢”)错误合并成“谢”,所以先不加后处理,直接看裸输出再判断问题出在哪一步。
4.4 现象:wav 读取报错,或者音频长度和特征维度对不上
有些录音是 44.1kHz 采样的音乐片段,有些是 8kHz 电话语音,直接送进为 16kHz 设计的模型,特征帧数会差好几倍,导致 feed_dict 时维度不一致。还有可能是 wav 本身是 int32 格式,wavfile.read返回的 dtype 不是 int16,除 32768 以后数值变得很奇怪。
解决方式:在 TFDataUtils.py 里加一个统一入口函数,强制sr=16000、mono=True、np.float32。我一般会在读取后加两行断言,一个是assert len(signal.shape) == 1,另一个是assert signal.dtype == np.float32,一旦数据没对齐立刻报错而不是等到训练中途才炸。
4.5 现象:GPU 显存不足,或者 CPU 训练慢到怀疑人生
课程设计源码通常没做过性能优化,如果直接把所有音频 pad 到全局最长长度,显存会被无用的 padding 占满。另一个原因是 batch_size 设置太大,再叠加双向 GRU 和卷积层,一张 4GB 显存的卡很容易 OOM。
解决方式:把 batch_size 从源码默认值减半,比如 64 改成 32。同时按音频长度排序后分组,让同一个 batch 内的样本时长相近,减少 padding 比例。这个方法叫 bucketing,不需要改模型代码,只要在数据迭代器里按seq_len排个序就行。CPU 训练时建议把n_mel降到 40,rnn_units降到 128,能明显提升迭代速度。
5. 换自己的录音验证:单文件预测与批量小改造
课程设计源码自带的 demo 音频跑通只是第一步,真正答辩时要能拿你自己的录音识别出来。这一章教你加两个小脚本,一个识别单文件,一个批量识别目录下所有 wav,最后还能输出简单的时间戳。
5.1 单文件预测:加载 checkpoint、对齐预处理、CTC 解码去重
假设你已经训练出 checkpoint,预测脚本的核心流程是:读音频 → 转 FBank → 按模型要求调整维度 → 输入模型拿 logits → CTC 解码 → 索引转字符串。代码骨架如下:
import tensorflow as tf from TFDataUtils import read_wav_to_signal, get_fbank def predict_one(path, sess, X, seq_len, logits, char_list, blank_index): signal = read_wav_to_signal(path) fbank = get_fbank(signal) # [time, n_mel] x = fbank[np.newaxis, :, :, np.newaxis].astype(np.float32) feed_len = int(np.ceil(fbank.shape[0] / 4)) # 两层 pool 压缩 4 倍 logits_val, = sess.run([logits], feed_dict={X: x, seq_len: [feed_len]}) logits_t = tf.constant(logits_val.transpose(1, 0, 2)) # [time, batch, num_classes] decoded, _ = tf.nn.ctc_greedy_decoder(logits_t, [feed_len]) # 返回 [batch, 未定长] 的稀疏张量,取第一个样本 indices = decoded[0].indices.numpy() values = decoded[0].values.numpy() result = [] for v in values: if v != blank_index: result.append(char_list[v]) return ''.join(result)逻辑说明:feed_len必须和模型输出时间步一致,这里写死除以 4,是因为模型里正好有两层 stride=2 的池化。如果你的模型多加了一层池化,这里要改成除以 8。logits_val.transpose(1, 0, 2)把网络输出的[batch, time, num_classes]转成[time, batch, num_classes],因为ctc_greedy_decoder要求 time-major。
参数说明:char_list是字符串列表,索引和 TFDataUtils.py 里的字符表必须完全一致,漏一个字符就会出现乱码。blank_index的值参考第 4 章排除法,建议打印values里实际出现的索引,再看哪个索引对应空白。解码结果通常已经去重删空,但如果你发现还有重复,可以在循环里加一个if v != prev的简单判断。
5.2 批量预测:写一个 predict_batch.py 处理一个目录的 wav
单文件调试通过后,批量识别就很自然了。为了提高 GPU 利用率,按音频长度排序后用动态 batch 处理:
import glob, numpy as np def predict_dir(wav_dir, sess, X, seq_len, logits, char_list, blank_index, batch_size=8): paths = glob.glob(f'{wav_dir}/*.wav') features = [(p, read_wav_to_signal(p)) for p in paths] features.sort(key=lambda x: x[1].shape[0]) # 按原始帧数排 for start in range(0, len(features), batch_size): batch = features[start:start + batch_size] batch_x, batch_len = [], [] for p, sig in batch: fb = get_fbank(sig) batch_x.append(fb[np.newaxis, :, :, np.newaxis]) batch_len.append(int(np.ceil(fb.shape[0] / 4))) max_len = max(fb.shape[1] for fb in batch_x) padded = np.zeros([len(batch), max_len, batch_x[0].shape[2], 1], dtype=np.float32) for i, fb in enumerate(batch_x): padded[i, :fb.shape[1], :, :] = fb[0] # feed_dict 和单文件一样,这样循环里拿结果逻辑说明:按长度排序后,相邻样本的batch_len差别不大,padding 产生的时间维浪费最少。max_len取当前 batch 内的最大值,而不是全局最大值,内存占用更稳。这里padded的第一维是 batch 数量,第二维是当前 batch 最大 time 维。
参数说明:batch_size在 CPU 上建议 4~8,GPU 上可以试 32 以上。如果遇到 OOM,先减小 batch_size,而不是把max_len改成全局最大,后者反而更容易爆显存。批处理结果可以直接逐行写入result.txt,文件名和识别文本用制表符分隔,方便答辩时展示。
5.3 把识别结果带上时间戳:自己加一小段后处理
课程设计如果只需要“识别对错”,单文本输出就够了。但如果想做成演示系统,每个字对应的时间位置会让效果专业很多。CTC 解码出的稀疏张量里,indices的第二列就是每个字符在输出时间步上的位置,乘上帧移和时间压缩倍数就能得到秒级时间点:
time_seconds = decoded[0].indices[:, 1] * 0.01 * 4这里0.01是 10ms 帧移,4是模型时间压缩倍数。比如某字对应索引 20,那么它出现在第 20 × 0.04 ≈ 0.8 秒左右。如果想得到每个字持续多久,可以拿后一个字符的开始时间减去当前字符的开始时间,最后一个字的结束时间用音频总时长近似。做字幕或者划线展示都够用。
我第一次拿这套课程设计代码跑自己的声音时,十条有八条识别成乱码,最后发现是麦克风录音默认 44.1kHz,而源码假设 16kHz,特征帧数全对不上。从那次之后,我每次拿到语音识别的源码,第一件事就是用一行代码打印所有待测 wav 的采样率和声道数,再进训练和预测流程。这个习惯救了我后面好几次,希望帮到你。
本文还有配套的精品资源,点击获取