ResNet之mxnet、keras、pytorch实现

目录

1、ResNet50的mxnet实现

2、ResNet的keras实现

3、ResNet的pytorch实现

 

1、ResNet50的mxnet实现

具体网络结构可参见https://blog.csdn.net/qq_21046135/article/details/81674605

和 https://blog.csdn.net/seven_year_promise/article/details/69360488

from mxnet.gluon import nn
from mxnet import nd, init, gluon, autograd
import mxnet as mx
import gluonbook as gb
from mxnet.gluon.data.vision import transforms


lr = 0.1
num_epochs = 100
batch_size = 128
ctx = mx.gpu()

tansformer = transforms.Compose([
    transforms.ToTensor()
])

# 加载数据
train_data = gluon.data.vision.ImageFolderDataset("/home/user/cf/st/train")
test_data = gluon.data.vision.ImageFolderDataset("/home/user/cf/st/test")

train_iter = gluon.data.DataLoader(train_data.transform_first(tansformer), shuffle=True, batch_size=batch_size)
test_iter = gluon.data.DataLoader(test_data.transform_first(tansformer),shuffle=True, batch_size=batch_size)

class Residual(nn.Block):
    def __init__(self, num_channels, use_1x1conv=False, strides=1, **kwargs):
        super(Residual, self).__init__(**kwargs)
        self.conv1 = nn.Conv2D(num_channels, kernel_size=1, strides=strides)
        self.conv2 = nn.Conv2D(num_channels, kernel_size=3, strides=1, padding=1)
        self.conv3 = nn.Conv2D(num_channels*4, kernel_size=1, strides=1)
        if use_1x1conv:
            self.conv4 = nn.Conv2D(num_channels*4, kernel_size=1, strides=strides)
            self.bn4 = nn.BatchNorm()
        else:
            self.conv4 = None
            self.bn4 = None
        self.bn1 = nn.BatchNorm()
        self.bn2 = nn.BatchNorm()
        self.bn3 = nn.BatchNorm()

    def forward(self, x):
        y = nd.relu(self.bn1(self.conv1(x)))
        y = nd.relu(self.bn2(self.conv2(y)))
        y = self.bn3(self.conv3(y))
        if self.conv4:
            x = self.bn4(self.conv4(x))
        return nd.relu(x+y)

def resnet_block(num_channels, num_residuals, stride=1):
    blk = nn.Sequential()
    for i in range(num_residuals):
        if i==0 :
            blk.add(Residual(num_channels, use_1x1conv=True, strides=stride))   
        else:
            blk.add(Residual(num_channels))
    return blk

net = nn.Sequential()
net.add(
    nn.Conv2D(64, kernel_size=7, strides=2, padding=3),
    nn.BatchNorm(),
    nn.Activation('relu'),
    nn.MaxPool2D(pool_size=3, strides=2)
)
net.add(
    resnet_block(64, 3, stride=1),
    resnet_block(128, 4, stride=2),
    resnet_block(256, 6, stride=2),
    resnet_block(512, 3, stride=2)
)
# 平均池化
net.add(nn.GlobalAvgPool2D(),
        nn.Dense(2))

net.initialize(init=init.Xavier(), force_reinit=True, ctx=ctx)
trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate':lr})
loss = gluon.loss.SoftmaxCrossEntropyLoss()
gb.train_ch5(net, train_iter, test_iter, batch_size, trainer, ctx, num_epochs)

当self.bn4定义在self.bn3的后面时,会出现错误:mxnet.gluon.parameter.DeferredInitializationError: Parameter 'batchnorm8_gamma' has not been initialized yet because initialization was deferred. Actual initialization happens during the first forward pass. Please pass one batch of data through the network before accessing Parameters. You can also avoid deferred initialization by specifying in_units, num_features, etc., for network layers.

这是因为定义在后面的bn4,当条件判断为false的时候,不会使用到self.bn4,会出现DeferredInitializationError的错误,所以需要同时定义两种情况下的self.bn4

2、ResNet的keras实现

from keras.layers import Conv2D, BatchNormalization, MaxPooling2D, Activation, ZeroPadding2D, Input, Add,AveragePooling2D, Flatten, Dense
from keras.models import Model
from keras.preprocessing.image import ImageDataGenerator
from keras.callbacks import LearningRateScheduler
from keras.callbacks import ModelCheckpoint
from keras.utils import multi_gpu_model
from keras.optimizers import Adam
import keras.backend as K

def Bottleneck(x, filters, stage, block, s, short_cut):
    conv_name_base = 'res'+str(stage)+block+'_branch'
    bn_name_base = 'bn'+str(stage)+block+'_branch'
    
    residual = x    #shortcut
    x = Conv2D(filters=filters, kernel_size=(1,1), strides=(1,1), padding='valid', name=conv_name_base+'2a')(x)
    x = BatchNormalization(axis=3, name=bn_name_base+'2a')(x)
    x = Activation('relu')(x)
    
    x = Conv2D(filters=filters, kernel_size=(3,3), strides=(s,s), padding='same', name=conv_name_base+'2b')(x)
    x = BatchNormalization(axis=3, name=bn_name_base+'2b')(x)
    x = Activation('relu')(x)
    
    x = Conv2D(filters=filters*4, kernel_size=(1,1), strides=(1,1), padding='valid', name=conv_name_base+'2c')(x)
    x = BatchNormalization(axis=3, name=bn_name_base+'2c')(x)
    
    if short_cut:
        residual = Conv2D(filters=filters*4, kernel_size=(1,1), strides=(s,s), name=conv_name_base+'1')(residual)
        residual = BatchNormalization(axis=3, name=bn_name_base+'1')(residual)
    
    out = Add()([x, residual])
    out = Activation('relu')(out)
    return out

def ResNet50(input_shape, classes):
    x_input = Input(input_shape)
    x = ZeroPadding2D((3,3))(x_input)
    
    x = Conv2D(64, kernel_size=(7,7), strides=(2,2), name='conv1', use_bias=True, kernel_initializer='glorot_uniform')(x)
    x = BatchNormalization(axis=3, name='bn_conv1')(x)
    x = Activation('relu')(x)
    x = ZeroPadding2D((1,1))(x)   
    x = MaxPooling2D(pool_size=(3,3), strides=(2,2), padding='same')(x)
    
    # layer1 *3
    x = Bottleneck(x, 64, stage=2, block='a', s=1, short_cut=True)
    x = Bottleneck(x, 64, stage=2, block='b', s=1, short_cut=False)
    x = Bottleneck(x, 64, stage=2, block='c', s=1, short_cut=False)
    
    # layer2 *4
    x = Bottleneck(x, 128, stage=3, block='a', s=2, short_cut=True)
    x = Bottleneck(x, 128, stage=3, block='b', s=1, short_cut=False)
    x = Bottleneck(x, 128, stage=3, block='c', s=1, short_cut=False)
    x = Bottleneck(x, 128, stage=3, block='d', s=1, short_cut=False)
    
    # layer3 *6
    x = Bottleneck(x, 256, stage=4, block='a', s=2, short_cut=True)
    x = Bottleneck(x, 256, stage=4, block='b', s=1, short_cut=False)
    x = Bottleneck(x, 256, stage=4, block='c', s=1, short_cut=False)
    x = Bottleneck(x, 256, stage=4, block='d', s=1, short_cut=False)
    x = Bottleneck(x, 256, stage=4, block='e', s=1, short_cut=False)
    x = Bottleneck(x, 256, stage=4, block='f', s=1, short_cut=False)
    
    # layer4 *3
    x = Bottleneck(x, 512, stage=5, block='a', s=2, short_cut=True)
    x = Bottleneck(x, 512, stage=5, block='b', s=1, short_cut=False)
    x = Bottleneck(x, 512, stage=5, block='c', s=1, short_cut=False)
    
    x = AveragePooling2D((2,2), name='avg_pool')(x)
    x = Flatten()(x)
    x = Dense(classes, activation='softmax', name='fc'+str(classes))(x)
    
    model = Model(inputs=x_input, outputs=x, name='ResNet50')
    
    return model

def ResNet101(input_shape, classes):
    x_input = Input(input_shape)
    x = ZeroPadding2D((3,3))(x_input)
    
    x = Conv2D(64, kernel_size=(7,7), strides=(2,2), name='conv1', use_bias=True, kernel_initializer='glorot_uniform')(x)
    x = BatchNormalization(axis=3, name='bn_conv1')(x)
    x = Activation('relu')(x)
    x = ZeroPadding2D((1,1))(x)    
    x = MaxPooling2D(pool_size=(3,3), strides=(2,2), padding='same')(x)
    
    # layer1 *3
    x = Bottleneck(x, 64, stage=2, block='a', s=1, short_cut=True)
    x = Bottleneck(x, 64, stage=2, block='b', s=1, short_cut=False)
    x = Bottleneck(x, 64, stage=2, block='c', s=1, short_cut=False)
    
    # layer2 *4
    x = Bottleneck(x, 128, stage=3, block='a', s=2, short_cut=True)
    x = Bottleneck(x, 128, stage=3, block='b', s=1, short_cut=False)
    x = Bottleneck(x, 128, stage=3, block='c', s=1, short_cut=False)
    x = Bottleneck(x, 128, stage=3, block='d', s=1, short_cut=False)
    
    # layer3 *23
    x = Bottleneck(x, 256, stage=4, block='a', s=2, short_cut=True)
    x = Bottleneck(x, 256, stage=4, block='b', s=1, short_cut=False)
    x = Bottleneck(x, 256, stage=4, block='c', s=1, short_cut=False)
    x = Bottleneck(x, 256, stage=4, block='d', s=1, short_cut=False)
    x = Bottleneck(x, 256, stage=4, block='e', s=1, short_cut=False)
    x = Bottleneck(x, 256, stage=4, block='f', s=1, short_cut=False)
    
    x = Bottleneck(x, 256, stage=4, block='a_2', s=1, short_cut=False)
    x = Bottleneck(x, 256, stage=4, block='b_2', s=1, short_cut=False)
    x = Bottleneck(x, 256, stage=4, block='c_2', s=1, short_cut=False)
    x = Bottleneck(x, 256, stage=4, block='d_2', s=1, short_cut=False)
    x = Bottleneck(x, 256, stage=4, block='e_2', s=1, short_cut=False)
    x = Bottleneck(x, 256, stage=4, block='f_2', s=1, short_cut=False)
    
    x = Bottleneck(x, 256, stage=4, block='a_3', s=1, short_cut=False)
    x = Bottleneck(x, 256, stage=4, block='b_3', s=1, short_cut=False)
    x = Bottleneck(x, 256, stage=4, block='c_3', s=1, short_cut=False)
    x = Bottleneck(x, 256, stage=4, block='d_3', s=1, short_cut=False)
    x = Bottleneck(x, 256, stage=4, block='e_3', s=1, short_cut=False)
    x = Bottleneck(x, 256, stage=4, block='f_3', s=1, short_cut=False)
    
    x = Bottleneck(x, 256, stage=4, block='a_4', s=1, short_cut=False)
    x = Bottleneck(x, 256, stage=4, block='b_4', s=1, short_cut=False)
    x = Bottleneck(x, 256, stage=4, block='c_4', s=1, short_cut=False)
    x = Bottleneck(x, 256, stage=4, block='d_4', s=1, short_cut=False)
    x = Bottleneck(x, 256, stage=4, block='e_4', s=1, short_cut=False)
    
    # layer4 *3
    x = Bottleneck(x, 512, stage=5, block='a', s=2, short_cut=True)
    x = Bottleneck(x, 512, stage=5, block='b', s=1, short_cut=False)
    x = Bottleneck(x, 512, stage=5, block='c', s=1, short_cut=False)
    
    x = AveragePooling2D((2,2), name='avg_pool')(x)
    x = Flatten()(x)
    x = Dense(classes, activation='softmax', name='fc'+str(classes))(x)
    
    model = Model(inputs=x_input, outputs=x, name='ResNet101')
    
    return model

def ResNet152(input_shape, classes):
    x_input = Input(input_shape)
    x = ZeroPadding2D((3,3))(x_input)
    
    x = Conv2D(64, kernel_size=(7,7), strides=(2,2), name='conv1', use_bias=True, kernel_initializer='glorot_uniform')(x)
    x = BatchNormalization(axis=3, name='bn_conv1')(x)
    x = Activation('relu')(x)
    x = ZeroPadding2D((1,1))(x)    
    x = MaxPooling2D(pool_size=(3,3), strides=(2,2), padding='same')(x)
    
    # layer1   * 3
    x = Bottleneck(x, filters=64, stage=2, block='a', s=1, short_cut=True)
    x = Bottleneck(x, filters=64, stage=2, block='b', s=1, short_cut=False)
    x = Bottleneck(x, filters=64, stage=2, block='c', s=1, short_cut=False)
    
    
    # layer2  *8
    x = Bottleneck(x, filters=128, stage=3, block='a', s=2, short_cut=True)
    x = Bottleneck(x, filters=128, stage=3, block='b', s=1, short_cut=False)
    x = Bottleneck(x, filters=128, stage=3, block='c', s=1, short_cut=False)
    x = Bottleneck(x, filters=128, stage=3, block='d', s=1, short_cut=False)
    x = Bottleneck(x, filters=128, stage=3, block='e', s=1, short_cut=False)
    x = Bottleneck(x, filters=128, stage=3, block='f', s=1, short_cut=False)
    x = Bottleneck(x, filters=128, stage=3, block='g', s=1, short_cut=False)
    x = Bottleneck(x, filters=128, stage=3, block='h', s=1, short_cut=False)
    
    # layer3  * 36
    x = Bottleneck(x, filters=256, stage=4, block='a', s=2, short_cut=True)
    x = Bottleneck(x, filters=256, stage=4, block='b', s=1, short_cut=False)
    x = Bottleneck(x, filters=256, stage=4, block='c', s=1, short_cut=False)
    x = Bottleneck(x, filters=256, stage=4, block='d', s=1, short_cut=False)
    x = Bottleneck(x, filters=256, stage=4, block='e', s=1, short_cut=False)
    x = Bottleneck(x, filters=256, stage=4, block='f', s=1, short_cut=False)
    
    x = Bottleneck(x, filters=256, stage=4, block='a_2', s=1, short_cut=False)
    x = Bottleneck(x, filters=256, stage=4, block='b_2', s=1, short_cut=False)
    x = Bottleneck(x, filters=256, stage=4, block='c_2', s=1, short_cut=False)
    x = Bottleneck(x, filters=256, stage=4, block='d_2', s=1, short_cut=False)
    x = Bottleneck(x, filters=256, stage=4, block='e_2', s=1, short_cut=False)
    x = Bottleneck(x, filters=256, stage=4, block='f_2', s=1, short_cut=False)
    
    x = Bottleneck(x, filters=256, stage=4, block='a_3', s=1, short_cut=False)
    x = Bottleneck(x, filters=256, stage=4, block='b_3', s=1, short_cut=False)
    x = Bottleneck(x, filters=256, stage=4, block='c_3', s=1, short_cut=False)
    x = Bottleneck(x, filters=256, stage=4, block='d_3', s=1, short_cut=False)
    x = Bottleneck(x, filters=256, stage=4, block='e_3', s=1, short_cut=False)
    x = Bottleneck(x, filters=256, stage=4, block='f_3', s=1, short_cut=False)
    
    x = Bottleneck(x, filters=256, stage=4, block='a_4', s=1, short_cut=False)
    x = Bottleneck(x, filters=256, stage=4, block='b_4', s=1, short_cut=False)
    x = Bottleneck(x, filters=256, stage=4, block='c_4', s=1, short_cut=False)
    x = Bottleneck(x, filters=256, stage=4, block='d_4', s=1, short_cut=False)
    x = Bottleneck(x, filters=256, stage=4, block='e_4', s=1, short_cut=False)
    x = Bottleneck(x, filters=256, stage=4, block='f_4', s=1, short_cut=False)
    
    x = Bottleneck(x, filters=256, stage=4, block='a_5', s=1, short_cut=False)
    x = Bottleneck(x, filters=256, stage=4, block='b_5', s=1, short_cut=False)
    x = Bottleneck(x, filters=256, stage=4, block='c_5', s=1, short_cut=False)
    x = Bottleneck(x, filters=256, stage=4, block='d_5', s=1, short_cut=False)
    x = Bottleneck(x, filters=256, stage=4, block='e_5', s=1, short_cut=False)
    x = Bottleneck(x, filters=256, stage=4, block='f_5', s=1, short_cut=False)
    
    x = Bottleneck(x, filters=256, stage=4, block='a_6', s=1, short_cut=False)
    x = Bottleneck(x, filters=256, stage=4, block='b_6', s=1, short_cut=False)
    x = Bottleneck(x, filters=256, stage=4, block='c_6', s=1, short_cut=False)
    x = Bottleneck(x, filters=256, stage=4, block='d_6', s=1, short_cut=False)
    x = Bottleneck(x, filters=256, stage=4, block='e_6', s=1, short_cut=False)
    x = Bottleneck(x, filters=256, stage=4, block='f_6', s=1, short_cut=False)
    
    # layer4 * 3
    x = Bottleneck(x, filters=512, stage=5, block='a', s=2, short_cut=True)
    x = Bottleneck(x, filters=512, stage=5, block='b', s=1, short_cut=False)
    x = Bottleneck(x, filters=512, stage=5, block='c', s=1, short_cut=False)
    
    x = AveragePooling2D((2,2), name='avg_pool')(x)
    x = Flatten()(x)
    x = Dense(classes, activation='softmax', name='fc'+str(classes))(x)
    
    model = Model(inputs=x_input, outputs=x, name='ResNet152')
    return model

查看网络结构可以通过model = ResNet152(input_shape=(224, 224, 3), classes=1000)  print(model)

ResNet网络很有规律性,conv2_x、conv3_x、conv4_x、conv5_x每个模块无论重复多少遍,模块内第一次调用Bottleneck,shortcut都是要通过downsample后再add,并且步长是2

3、ResNet的pytorch实现

import torch.nn as nn
import math
import torch.utils.model_zoo as model_zoo

__all__ = ['ResNet', 'resnet18', 'resnet34', 'resnet50', 'resnet101', 'resnet152']


model_urls = {
    'resnet18': 'https://download.pytorch.org/models/resnet18-5c106cde.pth',
    'resnet34': 'https://download.pytorch.org/models/resnet34-333f7ec4.pth',
    'resnet50': 'https://download.pytorch.org/models/resnet50-19c8e357.pth',
    'resnet101': 'https://download.pytorch.org/models/resnet101-5d3b4d8f.pth',
    'resnet152': 'https://download.pytorch.org/models/resnet152-b121ed2d.pth',
}

def conv3x3(in_planes, out_planes, stride=1):
    """3x3 convolution with padding"""
    return nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride,
                     padding=1, bias=False)


class BasicBlock(nn.Module):
    # Figure5(左) Block
    expansion = 1

    def __init__(self, inplanes, planes, stride=1, downsample=None):
        super(BasicBlock, self).__init__()
        self.conv1 = conv3x3(inplanes, planes, stride)
        self.bn1 = nn.BatchNorm2d(planes)
        self.relu = nn.ReLU(inplace=True)
        self.conv2 = conv3x3(planes, planes)
        self.bn2 = nn.BatchNorm2d(planes)
        self.downsample = downsample
        self.stride = stride

    def forward(self, x):
        residual = x

        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)

        out = self.conv2(out)
        out = self.bn2(out)

        if self.downsample is not None:
            residual = self.downsample(x)

        out += residual
        out = self.relu(out)

        return out


class Bottleneck(nn.Module):
        # Figure5(右) Block
    expansion = 4

    def __init__(self, inplanes, planes, stride=1, downsample=None):
        super(Bottleneck, self).__init__()
        self.conv1 = nn.Conv2d(inplanes, planes, kernel_size=1, bias=False)
        self.bn1 = nn.BatchNorm2d(planes)
        self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=stride,
                               padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(planes)
        self.conv3 = nn.Conv2d(planes, planes * self.expansion, kernel_size=1, bias=False)
        self.bn3 = nn.BatchNorm2d(planes * self.expansion)
        self.relu = nn.ReLU(inplace=True)
        self.downsample = downsample
        self.stride = stride

    def forward(self, x):
        residual = x

        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)

        out = self.conv2(out)
        out = self.bn2(out)
        out = self.relu(out)

        out = self.conv3(out)
        out = self.bn3(out)

        if self.downsample is not None:
            residual = self.downsample(x)

        out += residual
        out = self.relu(out)

        return out


class ResNet(nn.Module):

    def __init__(self, block, layers, num_classes=1000):
        self.inplanes = 64
        super(ResNet, self).__init__()
        self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3,
                               bias=False)
        self.bn1 = nn.BatchNorm2d(64)
        self.relu = nn.ReLU(inplace=True)
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        self.layer1 = self._make_layer(block, 64, layers[0])
        self.layer2 = self._make_layer(block, 128, layers[1], stride=2)
        self.layer3 = self._make_layer(block, 256, layers[2], stride=2)
        self.layer4 = self._make_layer(block, 512, layers[3], stride=2)
        self.avgpool = nn.AvgPool2d(7, stride=1)
        self.fc = nn.Linear(512 * block.expansion, num_classes)

        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
            elif isinstance(m, nn.BatchNorm2d):
                nn.init.constant_(m.weight, 1)
                nn.init.constant_(m.bias, 0)

    def _make_layer(self, block, planes, blocks, stride=1):
        downsample = None
        if stride != 1 or self.inplanes != planes * block.expansion:
            downsample = nn.Sequential(
                nn.Conv2d(self.inplanes, planes * block.expansion,
                          kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(planes * block.expansion),
            )

        layers = []
        layers.append(block(self.inplanes, planes, stride, downsample))
        self.inplanes = planes * block.expansion
        for i in range(1, blocks):
            layers.append(block(self.inplanes, planes))

        return nn.Sequential(*layers)

    def forward(self, x):
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu(x)
        x = self.maxpool(x)

        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)

        x = self.avgpool(x)
        x = x.view(x.size(0), -1)
        x = self.fc(x)

        return x


def resnet18(pretrained=False, **kwargs):
    """Constructs a ResNet-18 model.

    Args:
        pretrained (bool): If True, returns a model pre-trained on ImageNet
    """
    model = ResNet(BasicBlock, [2, 2, 2, 2], **kwargs)
    if pretrained:
        model.load_state_dict(model_zoo.load_url(model_urls['resnet18']))
    return model


def resnet34(pretrained=False, **kwargs):
    """Constructs a ResNet-34 model.

    Args:
        pretrained (bool): If True, returns a model pre-trained on ImageNet
    """
    model = ResNet(BasicBlock, [3, 4, 6, 3], **kwargs)
    if pretrained:
        model.load_state_dict(model_zoo.load_url(model_urls['resnet34']))
    return model


def resnet50(pretrained=False, **kwargs):
    """Constructs a ResNet-50 model.

    Args:
        pretrained (bool): If True, returns a model pre-trained on ImageNet
    """
    model = ResNet(Bottleneck, [3, 4, 6, 3], **kwargs)
    if pretrained:
        model.load_state_dict(model_zoo.load_url(model_urls['resnet50']))
    return model


def resnet101(pretrained=False, **kwargs):
    """Constructs a ResNet-101 model.

    Args:
        pretrained (bool): If True, returns a model pre-trained on ImageNet
    """
    model = ResNet(Bottleneck, [3, 4, 23, 3], **kwargs)
    if pretrained:
        model.load_state_dict(model_zoo.load_url(model_urls['resnet101']))
    return model


def resnet152(pretrained=False, **kwargs):
    """Constructs a ResNet-152 model.

    Args:
        pretrained (bool): If True, returns a model pre-trained on ImageNet
    """
    model = ResNet(Bottleneck, [3, 8, 36, 3], **kwargs)
    if pretrained:
        model.load_state_dict(model_zoo.load_url(model_urls['resnet152']))
    return model

猜你喜欢

转载自blog.csdn.net/jin__9981/article/details/89477397