ValueTpye boxing及虚方法重写及CallVirt指令实现解析

问题的提出,是源自Justin提出的一个case里面的一个问题,讨论了n久没得到一个答案,昨天justin周一早上一起来就又回忆起了这个问题,看来一直把这个问题放在脑子里面没有放下,佩服啊佩服 ^_^ 遂决定深入研究一番,下面是问题的提出:

 

Boxed value type

In C#, the value type instance having pure user data is resided at stack without any type pointer. In some case, the value need be boxed, then a new boxed object is created from heap. My questions are:

l         Assume the value type overrides an virtual function, such as ToString(). When call the function using value instance, the this points to the pure user data part of value instance in stack, however, if calling function using boxed value object, the this should point to the beginning of heap object (the type pointer, four bytes before pure user data). How does the ToString() implementation of value type distinguish between the two cases?

From debug of assembly instruction, I find the ToString() function always gets this pointing to the pure user data part even if it’s called from boxed object. But I have no idea which code move the this pointer forward four bytes?

 

       首先,对于什么是值类型啥是引用类型,及其区别,以及派生结构层次,不属于这里的话题,探讨主要围绕针对上面的这个问题的提出展开。

       可以定义一个Struct的类型,来实现对ValueType的继承。这个从Struct继承出来的ValueType,还可以重写从基类继承的方法,一共只有三个方法可供重写:Equals(),

GetHashCode(),ToString()

       好吧,看看上面的问题,首先解释下上面的问题提出之前的一段文字:

 

       Assume the value type overrides an virtual function, such as ToString(). When call the function using value instance, the this points to the pure user data part of value instance in stack, however, if calling function using boxed value object, the this should point to the beginning of heap object (the type pointer, four bytes before pure user data).

      

       厄,首先明确一下,这段话是没有任何问题的。我也曾怀疑过这句话里面的细微的地方的观点,但是事实证明我是错误的…..

       来构造一个所提出问题的案例先:

       class Program

    {

        static void Main(string[] args)

        {

            TestValueType testValueType = new TestValueType(1214926);

            Console.WriteLine(testValueType.ToString());

 

            Object o = testValueType;

            Console.WriteLine(o.ToString());

 

            int i = 12345;

            Console.WriteLine(i.ToString());

 

            Console.ReadLine();

        }

 

        internal struct TestValueType

        {

            private int i;

            public TestValueType(int initValue)

            {

                i = initValue;

            }

 

            //Can only override 3 Methods:Equals(),GetHashCode(),ToString()

            public override string ToString()

            {

                return "Valuetype virtual method tostring() Override.";

            }

        }

    }

      

       对于提出的第一个问题:

 

How does the ToString() implementation of value type distinguish between the two cases?

 

       我想,这个地方不仅仅是two case,而是有四种case

1.         一个自定义的值类型没有重写tostring()方法的时候,对tostring的调用是如何实现的?tostring()方法的实现是存储在什么地方的?

2.         一个自定义的值类型overridevirtualbase classtostring方法之后,对tostring的调用是如何进行的?这个tostring的实现是存放在什么地方的?也就是调用的哪个实现,如何调用的问题。

3.         一个boxed了的自定义的值类型的boxed状态下,没有重写tostring方法的时候,这个tostring是如何调用的?

4.         上面的一个问题,一个重写了tostring方法的自定义valuetypeboxed状态下面,tostring的调用是如何实现的,存放在哪儿。

 

Ok,这个是回答他的问题的序言,当然,不是全部,还有Managed PointerInstance Pointerthis指针的关系callvirt的具体细节和几个il指令背着我做的小动作问题..

      

       好吧,解决这些问题,先从il语言入手,下面是Main方法的反编译之后的il代码:

 

.method private hidebysig static void  Main(string[] args) cil managed

{

  .entrypoint

  // Code size       78 (0x4e)

  .maxstack  2

  .locals init ([0] valuetype ValutTypeTest.Program/TestValueType

testValueType,

           [1] object o,

           [2] int32 i)

  IL_0000:  nop

  IL_0001:  ldloca.s   testValueType

  IL_0003:  ldc.i4     0x1289ce

  IL_0008:  call       instance void

ValutTypeTest.Program/TestValueType::.ctor(int32)

  IL_000d:  nop

  IL_000e:  ldloca.s   testValueType

  IL_0010:  constrained. ValutTypeTest.Program/TestValueType

  IL_0016:  callvirt   instance string [mscorlib]System.Object::ToString()

  IL_001b:  call       void [mscorlib]System.Console::WriteLine(string)

  IL_0020:  nop

  IL_0021:  ldloc.0

  IL_0022:  box        ValutTypeTest.Program/TestValueType

  IL_0027:  stloc.1

  IL_0028:  ldloc.1

  IL_0029:  callvirt   instance string [mscorlib]System.Object::ToString()

  IL_002e:  call       void [mscorlib]System.Console::WriteLine(string)

  IL_0033:  nop

  IL_0034:  ldc.i4     0x3039

  IL_0039:  stloc.2

  IL_003a:  ldloca.s   i

  IL_003c:  call       instance string [mscorlib]System.Int32::ToString()

  IL_0041:  call       void [mscorlib]System.Console::WriteLine(string)

  IL_0046:  nop

  IL_0047:  call       string [mscorlib]System.Console::ReadLine()

  IL_004c:  pop

  IL_004d:  ret

} // end of method Program::Main

 

首先说说几个pointer的关系。

对于一个放在计算堆栈里面的value type或者是自定义value type来说,如果想要使用这个类型里面的fields或者是members,需要提供Managed Pointer。也就是&valuetypename的值。在C++里面,没有类似的Dotnet的值类型之内的概念,所以,这里的Managed Pointer就相当于一个objectinstance Pointer,或者说是instance reference。对于Value TypeManaged Pointer,指向的是计算stack上面的data part。而对于一个对象来说,this pointerinstance Pointer)指向的是Obj ref,一个四个字节的地方。这个地方存放的数据,指向的是MethodTable。这四个字节的上面,是控制同步的一个块和一个Object Header,这四个字节的下面,就是instance fields,包括一些fields和一些member方法的实现。

       This指针在对value type的时候,指向的是stack上面的数据部分。

 

       对于il的一个call方法指令,不管是value type,还是ref type,有的时候是需要指针的,有的时候不需要this指针。Metadata里面并不包含this指针,但是方法的签名,使用一个叫做HASTHISbit位来标识是否需要this指针:Signature: If the method is static, then the HASTHIS (0x20) bit in the calling convention shall be 0”。具体说来,如果一个方法的实现,是保持在type相关的内存中的,就不要这个标识位,如果每个typeinstance保存一份实现,就用这个标识。区别这两种情况,可以用C#里面的Stact关键字。

      

       对于:

Object o = testValueType;

Console.WriteLine(o.ToString());

 

Il代码是:

 

IL_000e:  ldloca.s   testValueType

IL_0010:  constrained. ValutTypeTest.Program/TestValueType

IL_0016:  callvirt   instance string [mscorlib]System.Object::ToString()

IL_001b:  call       void [mscorlib]System.Console::WriteLine(string)

      

       Constrained前缀修饰符,是一个非常有意思的修饰符,他必须和callvirt关键字一起使用,首先看起Stack转换:

 

constrained. thisType(这里是ValutTypeTest.Program/TestValueType

 

Stack Transition:

之前:…, ptr, arg1, … argN     之后:…, ptr, arg1, … argN

 

Ms没有什么变换,但是PtrManaged Pointer)的内容,却是有一个转换逻辑:

l         If thisType is a reference type (as opposed to a value type) thenptr is dereferenced and passed as the ‘this’ pointer to the callvirt of method

l         If thisType is a value type and thisType implements method thenptr is passed unmodified as the ‘this’ pointer to a call of method implemented by thisType  

l         If thisType is a value type and thisType does not implement method thenptr is dereferenced, boxed, and passed as the ‘this’ pointer to the callvirt of method

 

这样,就得到了问题的回答

对于一个value type(或者自定义的value type),仍然可以调用由类型继承或者是从重写的虚方法。比如EqualsGetHashCode,或者Tostring,因为CLR正好可以以非虚的方式调用这些方法。

如果重写了一个自定义的value type类型的tostring方法,首先,this pointermanaged Pointer)指向的是stack上面的这个变量的data field,数据开始部分。然后在调用tostring方法的时候,首先让constrained来检验下,接着发现了这个自定义的value type实现了tostring这个方法,好,这个时候不执行boxing的动作,直接采用il指令里面的call指令,然后直接调用stack上面的这个value typedata filed之后的tostring方法。

这种情况下,是没有MethodTable,不和MethodTable进行交互。很重要的一点,值类型隐式为sealed类型,所以不可将一个值类型做为另外一个类型的基类使用。

如果自定义的value type类型,没有实现了tostring方法,这个时候,constrained前缀的处理逻辑是根据“ldloca.s testValueType”刚刚放到stack上面的value typemanaged pointer,先把这个value type boxing,装箱,然后把在heap上面新创建的objectref,替换掉这个ptrvalue typemanaged pointer),(很重要的一点:这个时候,this指针指向的是obj ref。这个时候,就换成了使用callvirt指令调用。这个时候,ptr上面的内容,就是object refheap上面的value typeinstace fields的前四个字节的地方。这个obj ref里面的内容,指向的就是MethodTable。这个时候调用virtual方法,需要通过Vtable map,来具体定位到使用那个基类的方法。

寻找的流程是先看看当前的instance里面实现了相同签名的方法没,如果没有,就找基类或者父类里面的相同签名的。

 

如果,calling function using boxed value object,这个时候是如何实现的呢?还是上面的C#代码,当如下面操作的时候:

Object o = testValueType;

Console.WriteLine(o.ToString());

 

不管这个时候testValueType实现没实现tostring方法,这个时候,想当于直接调用一个Object Type的某个方法,是通过走MethodTable来寻找其实现的。

这种情况下,无论如何,在刚刚准备执行这个方法之前,this指针的内容,是一个Ref Typeinstance fields的开始的部分。而具体的使用哪个方法,则是根据MethodTable来的。

 

另外一点需要了解,CLR的特性里面,提供可以以非虚的方式调用从类型或者基类继承的方法。而且System.ValueType重写了这些虚方法,由struct定义的自定义value type没重写这些方法。

了解了这点,如何还有疑惑的话,可以看下下面这个有助于理解的sample

.class public value XXX

{

.method public void YYY( )

{

...

}

.method public virtual void ZZZ( )

{

...

}

}

.method public static void Exec( )

{

.entrypoint

.locals init(valuetype XXX xxx)  // Variable xxx is an Instance of XXX

ldloca xxx                               // Load managed ptr to xxx

call instance void XXX::YYY( )  // Legal: access to value

// type member

// by managed ptr

ldloca xxx

callvirt instance void XXX::ZZZ( ) // Illegal: virtual call of

// methods possible only

// by object reference.

ldloca xxx

call instance void XXX::ZZZ( )  // Legal: nonvirtual call, access to value type member

// by managed ptr.

ldloc xxx                                  // Load instance of XXX.

box valuetype XXX                    // Convert it to object reference.

callvirt instance void XXX::ZZZ( ) // Legal

...

}

 

 

这时,就涉及到文章最开始提出的第二个问题

 

From debug of assembly instruction, I find the ToString() function always gets this pointing to the pure user data part even if it’s called from boxed object. But I have no idea which code move the this pointer forward four bytes?

 

这里,有一个比较重要的特性,也是callvirt指令来实现的,在具体调用每个方法开始的时候之前,callvirt实现了一个justin和问题的提出者,叫做“this指针偏移”的功能。当然,文章到此为止,还没有证明这一点。

这个解释逻辑,也是参考了大量的资料之后得到的一个假设吧。Callvirt,在执行的时候,有很多种不同的情况下都可以调用callvirt,譬如interface,虚方法,多态等等,callvirt会进行一个判断,来判断具体是哪种情况。如果是我们上面的对boxed value type的情况,就有一个this指针偏移的处理逻辑。

 

为了证明这点,咱可以参考Rotor是如何实现callvirt方法的:

首先查看Fjit.cpp的实现,这个页面有万把行,实现了大部分IL指令具体做了些啥。

 

switch (opcode)

{

case CEE_CALLVIRT:

JitResult = compileCEE_CALLVIRT();

break;

}

 

好吧,查看compileCEE_CALLVIRT的实现:

 

FJitResult FJit::compileHelperCEE_CALLVIRT(unsigned int token,

                              bool isReadOnly /* = false */)

{

    jitInfo->getCallInfo(methodInfo->ftn,

                         tokenScope,

                         token,

                         0, // constraintToken -                                                            

                         tokenContext,

                         CORINFO_CALLINFO_CALLVIRT,

                         &virtCallInfo);

 

    if (virtCallInfo.kind == CORINFO_VIRTUALCALL_LDVIRTFTN)

    {

       int this_ptr = findOffsetOfThisPtr(targetSigInfo);

       emit_getSP((STACK_BUFFER + this_ptr - SIZE_STACK_SLOT));

       emit_LDIND_I4(false);

       emit_ldvirtftn_helper(token, jitInfo->getMemberParent(methodInfo->scope, token));

       emit_save_TOS();        // squirel away the target ftn address

       emit_POP_PTR();         // and remove from stack

    }

 

    argBytes = buildCall(&targetSigInfo, CALL_NONE, stackPadorRetBase, false);

 

    sizeRetBuff = targetSigInfo.hasRetBuffArg() ? typeSizeInBytes(jitInfo, targetSigInfo.retTypeClass) : 0;

 

    _ASSERTE (virtCallInfo.kind != CORINFO_CALL_CODE_POINTER);

 

    if (virtCallInfo.kind == CORINFO_VIRTUALCALL_LDVIRTFTN)

    {

        emit_restore_TOS(); //push the saved target ftn address

 

        // Now we can use the sequence for CALLI.

        emit_calli(targetSigInfo.hasRetBuffArg() ? typeSizeInBytes(jitInfo,

targetSigInfo.retTypeClass) : 0);

    }

    else if (virtCallInfo.kind == CORINFO_VIRTUALCALL_STUB)

    {

        _ASSERTE (!virtCallInfo.stubLookup.lookupKind.needsRuntimeLookup);

        _ASSERTE (virtCallInfo.stubLookup.constLookup.addr != NULL);

        _ASSERTE(virtCallInfo.stubLookup.constLookup.accessType == IAT_PVALUE);

        emit_call_stub((unsigned int) virtCallInfo.stubLookup.constLookup.addr);

    }

    else if (virtCallInfo.kind == CORINFO_CALL)

    {

        if (virtCallInfo.nullInstanceCheck)

        {

            emit_check_null_reference(false);

        }

 

        CORINFO_CONST_LOOKUP addrInfo;

        jitInfo->getFunctionEntryPoint(targetMethod, IAT_VALUE, &addrInfo);

        VALIDITY_CHECK(addrInfo.addr);

        VALIDITY_CHECK(addrInfo.accessType == IAT_VALUE ||

addrInfo.accessType == IAT_PVALUE);

 

        emit_callnonvirt((unsigned)addrInfo.addr, sizeRetBuff, addrInfo.accessType == IAT_PVALUE);

    }

    else if (virtCallInfo.kind == CORINFO_VIRTUALCALL_VTABLE)

    {

        if (jitInfo->getClassAttribs(targetClass,methodInfo->ftn) &

CORINFO_FLG_INTERFACE)

        {

            offset = jitInfo->getMethodVTableOffset(targetMethod);

            _ASSERTE(!(methodAttribs & CORINFO_FLG_EnC));

            unsigned InterfaceTableOffset;

            InterfaceTableOffset = jitInfo->getInterfaceTableOffset(targetClass);

            emit_callinterface_new(InterfaceTableOffset*4,

                offset, sizeRetBuff );

        }

        else

        {

 

            offset = jitInfo->getMethodVTableOffset(targetMethod);

            _ASSERTE(!(methodAttribs & CORINFO_FLG_DELEGATE_INVOKE));

            emit_callvirt(offset, sizeRetBuff);

        }

    }

}

 

这里只截取了最后的一段,前面的完整性检查之内的略掉。virtCallInfo,看到这样的结构,字眼和判断,和咱估计的情况差不多。然后来查看CORINFO_CALL_KIND这个结构体的定义:

 

enum CORINFO_CALL_KIND

{

    CORINFO_CALL,

       //下面的两个CallVirt指令里面没用

    CORINFO_CALL_CODE_POINTER,

CORINFO_VIRTUALCALL_RESOLVED,

 

    CORINFO_VIRTUALCALL_STUB,

    CORINFO_VIRTUALCALL_LDVIRTFTN,

    CORINFO_VIRTUALCALL_VTABLE

};

 

转到corinfo.h文件里面,getCallInfo and CORINFO_CALL_INFO,这两个东西是EE用来指示Fjit如何具体编译不同情况下面的callvirt指令。

查看都是什么情况下使用不同的结构体,发现了

CORINFO_VIRTUALCALL_LDVIRTFTN这种情况下对this指针偏移的支持:

      

       //找到this指针的地址

       int this_ptr = findOffsetOfThisPtr(targetSigInfo);

       //减去四个字节指向到instance fields

       // #define SIZE_STACK_SLOT      4     

emit_getSP((STACK_BUFFER + this_ptr - SIZE_STACK_SLOT));

emit_LDIND_I4(false);

emit_ldvirtftn_helper(token, jitInfo->getMemberParent(methodInfo->scope, token));

emit_save_TOS();        // squirel away the target ftn address

emit_POP_PTR();         // and remove from stack

 

emit_getSP方法最终指向了xp平台下x86fjit.h的实现:

// push a pointer pointing 'n' bytes back in the stack

#define x86_getSP(n)                                                                                      deregisterTOS;                                                                                                 

       if (n == 0)                                                                                                

           x86_mov_reg(x86DirTo, x86Big, x86_mod_reg(X86_EAX, X86_ESP));  

       else                                                                                                          

              x86_lea(x86_mod_base_scale_disp(X86_EAX, X86_ESP,

X86_NO_IDX_REG, n, 0));  

inRegTOS = true;

 

       最后,再说下,再看这些il指令的实现的时候,发现constrained的实现比较有意思,也就是基于上面给出的三种判断逻辑情况下,使用了一个结构体:

 

enum CORINFO_THIS_TRANSFORM

{

    CORINFO_NO_THIS_TRANSFORM,

    CORINFO_BOX_THIS,

    CORINFO_DEREF_THIS

};

       来支持三种不同情况下面是this指针的转换方式:

    switch (callInfo.thisTransform)

    {

    case CORINFO_NO_THIS_TRANSFORM:

        {

                     //不需要改变this指针(managed Pointerptr)的情况下直接调用call指令

            return this->compileHelperCEE_CALL(funcToken,

callInfo.targetMethodHandle,false /*readonly*/);

        }

//根据managed ptr的内容来装箱(If thisType is a value type and thisType implements //method)

    case CORINFO_BOX_THIS:

        {

            CORINFO_SIG_INFO targetSigInfo;

            jitInfo->getMethodSig(callInfo.targetMethodHandle, &targetSigInfo);

            // this is slightly ineffecient, especially when dealing with large

            // valuetypes but effeciency is not paramount in fjit

 

            // {... , objPtr, args} -> {..., objPtr, args, objPtr }

            copyPtrUpAroundArgs(targetSigInfo);

 

            // {..., objPtr, args, objPtr } -> {..., objPtr, args, *objPtr }

            if( (retval = this->compileHelperCEE_LDOBJ(constraintToken)) != FJIT_OK)

                return retval;

 

            // {..., objPtr, args, *objPtr } -> {..., objPtr, args, boxedPtr }

            if( (retval = this->compileHelperCEE_BOX(constraintToken)) != FJIT_OK)

                return retval;

 

            // {..., objPtr, args, boxedPtr } -> {... , boxedPtr, args}

            copyPtrDownAroundArgs(targetSigInfo);

 

            return this->compileHelperCEE_CALLVIRT(funcToken);

        }

       //最后一种情况(If thisType is a value type and thisType does not implement method),直接

//调用CALLVIRT

    case CORINFO_DEREF_THIS:

        {

            CORINFO_SIG_INFO targetSigInfo;

            jitInfo->getMethodSig(callInfo.targetMethodHandle, &targetSigInfo);

 

            // {... , &this, args} -> {..., &this, args, &this }

            copyPtrUpAroundArgs(targetSigInfo);

 

            // it was a reference type

            if( (retval = this->compileCEE_LDIND_REF()) != FJIT_OK)

                return retval;

 

            // {... , &this, args, this} -> {..., this, args}

            copyPtrDownAroundArgs(targetSigInfo);

 

            return this->compileHelperCEE_CALLVIRT(funcToken);

        }

 

       文章写的匆忙,很多看资料的时候看到的细节可能忘记了没写上。欢迎大伙讨论

       有写的不正确的地方,欢迎指正。^_^

 

       6/24/2008 4:57:24 PM  lbq1221119

1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。
应用背景为变电站电力巡检,基于YOLO v4算法模型对常见电力巡检目标进行检测,并充分利用Ascend310提供的DVPP等硬件支持能力来完成流媒体的传输、处理等任务,并对系统性能做出一定的优化。.zip深度学习是机器学习的一个子领域,它基于人工神经网络的研究,特别是利用多层次的神经网络来进行学习和模式识别。深度学习模型能够学习数据的高层次特征,这些特征对于图像和语音识别、自然语言处理、医学图像分析等应用至关重要。以下是深度学习的一些关键概念和组成部分: 1. **神经网络(Neural Networks)**:深度学习的基础是人工神经网络,它是由多个层组成的网络结构,包括输入层、隐藏层和输出层。每个层由多个神经元组成,神经元之间通过权重连接。 2. **前馈神经网络(Feedforward Neural Networks)**:这是最常见的神经网络类型,信息从输入层流向隐藏层,最终到达输出层。 3. **卷积神经网络(Convolutional Neural Networks, CNNs)**:这种网络特别适合处理具有网格结构的数据,如图像。它们使用卷积层来提取图像的特征。 4. **循环神经网络(Recurrent Neural Networks, RNNs)**:这种网络能够处理序列数据,如时间序列或自然语言,因为它们具有记忆功能,能够捕捉数据中的时间依赖性。 5. **长短期记忆网络(Long Short-Term Memory, LSTM)**:LSTM 是一种特殊的 RNN,它能够学习长期依赖关系,非常适合复杂的序列预测任务。 6. **生成对抗网络(Generative Adversarial Networks, GANs)**:由两个网络组成,一个生成器和一个判别器,它们相互竞争,生成器生成数据,判别器评估数据的真实性。 7. **深度学习框架**:如 TensorFlow、Keras、PyTorch 等,这些框架提供了构建、训练和部署深度学习模型的工具和库。 8. **激活函数(Activation Functions)**:如 ReLU、Sigmoid、Tanh 等,它们在神经网络中用于添加非线性,使得网络能够学习复杂的函数。 9. **损失函数(Loss Functions)**:用于评估模型的预测与真实值之间的差异,常见的损失函数包括均方误差(MSE)、交叉熵(Cross-Entropy)等。 10. **优化算法(Optimization Algorithms)**:如梯度下降(Gradient Descent)、随机梯度下降(SGD)、Adam 等,用于更新网络权重,以最小化损失函数。 11. **正则化(Regularization)**:技术如 Dropout、L1/L2 正则化等,用于防止模型过拟合。 12. **迁移学习(Transfer Learning)**:利用在一个任务上训练好的模型来提高另一个相关任务的性能。 深度学习在许多领域都取得了显著的成就,但它也面临着一些挑战,如对大量数据的依赖、模型的解释性差、计算资源消耗大等。研究人员正在不断探索新的方法来解决这些问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值